New shader cache implementation (#3194)

* New shader cache implementation

* Remove some debug code

* Take transform feedback varying count into account

* Create shader cache directory if it does not exist + fragment output map related fixes

* Remove debug code

* Only check texture descriptors if the constant buffer is bound

* Also check CPU VA on GetSpanMapped

* Remove more unused code and move cache related code

* XML docs + remove more unused methods

* Better codegen for TransformFeedbackDescriptor.AsSpan

* Support migration from old cache format, remove more unused code

Shader cache rebuild now also rewrites the shared toc and data files

* Fix migration error with BRX shaders

* Add a limit to the async translation queue

 Avoid async translation threads not being able to keep up and the queue growing very large

* Re-create specialization state on recompile

This might be required if a new version of the shader translator requires more or less state, or if there is a bug related to the GPU state access

* Make shader cache more error resilient

* Add some missing XML docs and move GpuAccessor docs to the interface/use inheritdoc

* Address early PR feedback

* Fix rebase

* Remove IRenderer.CompileShader and IShader interface, replace with new ShaderSource struct passed to CreateProgram directly

* Handle some missing exceptions

* Make shader cache purge delete both old and new shader caches

* Register textures on new specialization state

* Translate and compile shaders in forward order (eliminates diffs due to different binding numbers)

* Limit in-flight shader compilation to the maximum number of compilation threads

* Replace ParallelDiskCacheLoader state changed event with a callback function

* Better handling for invalid constant buffer 1 data length

* Do not create the old cache directory structure if the old cache does not exist

* Constant buffer use should be per-stage. This change will invalidate existing new caches (file format version was incremented)

* Replace rectangle texture with just coordinate normalization

* Skip incompatible shaders that are missing texture information, instead of crashing

This is required if we, for example, support new texture instruction to the shader translator, and then they allow access to textures that were not accessed before. In this scenario, the old cache entry is no longer usable

* Fix coordinates normalization on cubemap textures

* Check if title ID is null before combining shader cache path

* More robust constant buffer address validation on spec state

* More robust constant buffer address validation on spec state (2)

* Regenerate shader cache with one stream, rather than one per shader.

* Only create shader cache directory during initialization

* Logging improvements

* Proper shader program disposal

* PR feedback, and add a comment on serialized structs

* XML docs for RegisterTexture

Co-authored-by: riperiperi <rhy3756547@hotmail.com>
This commit is contained in:
gdkchan 2022-04-10 10:49:44 -03:00 committed by GitHub
parent 26a881176e
commit 43ebd7a9bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
81 changed files with 6421 additions and 2406 deletions

View file

@ -1,7 +1,12 @@
using Ryujinx.Graphics.Shader.Translation;
namespace Ryujinx.Graphics.GAL
{
public struct Capabilities
{
public readonly TargetApi Api;
public readonly string VendorName;
public readonly bool HasFrontFacingBug;
public readonly bool HasVectorIndexingBug;
@ -24,6 +29,8 @@ namespace Ryujinx.Graphics.GAL
public readonly int StorageBufferOffsetAlignment;
public Capabilities(
TargetApi api,
string vendorName,
bool hasFrontFacingBug,
bool hasVectorIndexingBug,
bool supportsAstcCompression,
@ -43,6 +50,8 @@ namespace Ryujinx.Graphics.GAL
float maximumSupportedAnisotropy,
int storageBufferOffsetAlignment)
{
Api = api;
VendorName = vendorName;
HasFrontFacingBug = hasFrontFacingBug;
HasVectorIndexingBug = hasVectorIndexingBug;
SupportsAstcCompression = supportsAstcCompression;

View file

@ -16,11 +16,9 @@ namespace Ryujinx.Graphics.GAL
void BackgroundContextAction(Action action, bool alwaysBackground = false);
IShader CompileShader(ShaderStage stage, string code);
BufferHandle CreateBuffer(int size);
IProgram CreateProgram(IShader[] shaders, ShaderInfo info);
IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info);
ISampler CreateSampler(SamplerCreateInfo info);
ITexture CreateTexture(TextureCreateInfo info, float scale);

View file

@ -4,7 +4,6 @@ using Ryujinx.Graphics.GAL.Multithreading.Commands.CounterEvent;
using Ryujinx.Graphics.GAL.Multithreading.Commands.Program;
using Ryujinx.Graphics.GAL.Multithreading.Commands.Renderer;
using Ryujinx.Graphics.GAL.Multithreading.Commands.Sampler;
using Ryujinx.Graphics.GAL.Multithreading.Commands.Shader;
using Ryujinx.Graphics.GAL.Multithreading.Commands.Texture;
using Ryujinx.Graphics.GAL.Multithreading.Commands.Window;
using System;
@ -53,8 +52,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
{
_lookup[(int)CommandType.Action] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
ActionCommand.Run(ref GetCommand<ActionCommand>(memory), threaded, renderer);
_lookup[(int)CommandType.CompileShader] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
CompileShaderCommand.Run(ref GetCommand<CompileShaderCommand>(memory), threaded, renderer);
_lookup[(int)CommandType.CreateBuffer] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
CreateBufferCommand.Run(ref GetCommand<CreateBufferCommand>(memory), threaded, renderer);
_lookup[(int)CommandType.CreateProgram] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
@ -98,9 +95,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_lookup[(int)CommandType.SamplerDispose] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
SamplerDisposeCommand.Run(ref GetCommand<SamplerDisposeCommand>(memory), threaded, renderer);
_lookup[(int)CommandType.ShaderDispose] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
ShaderDisposeCommand.Run(ref GetCommand<ShaderDisposeCommand>(memory), threaded, renderer);
_lookup[(int)CommandType.TextureCopyTo] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
TextureCopyToCommand.Run(ref GetCommand<TextureCopyToCommand>(memory), threaded, renderer);
_lookup[(int)CommandType.TextureCopyToScaled] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>

View file

@ -3,7 +3,6 @@
enum CommandType : byte
{
Action,
CompileShader,
CreateBuffer,
CreateProgram,
CreateSampler,
@ -29,8 +28,6 @@
SamplerDispose,
ShaderDispose,
TextureCopyTo,
TextureCopyToScaled,
TextureCopyToSlice,

View file

@ -1,22 +0,0 @@
using Ryujinx.Graphics.GAL.Multithreading.Model;
using Ryujinx.Graphics.GAL.Multithreading.Resources;
namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Renderer
{
struct CompileShaderCommand : IGALCommand
{
public CommandType CommandType => CommandType.CompileShader;
private TableRef<ThreadedShader> _shader;
public void Set(TableRef<ThreadedShader> shader)
{
_shader = shader;
}
public static void Run(ref CompileShaderCommand command, ThreadedRenderer threaded, IRenderer renderer)
{
ThreadedShader shader = command._shader.Get(threaded);
shader.EnsureCreated();
}
}
}

View file

@ -1,7 +1,4 @@
using Ryujinx.Graphics.GAL.Multithreading.Resources;
using Ryujinx.Graphics.Shader;
namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Renderer
namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Renderer
{
struct CreateBufferCommand : IGALCommand
{

View file

@ -1,6 +1,4 @@
using System;
namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Renderer
namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Renderer
{
struct PreFrameCommand : IGALCommand
{

View file

@ -1,21 +0,0 @@
using Ryujinx.Graphics.GAL.Multithreading.Model;
using Ryujinx.Graphics.GAL.Multithreading.Resources;
namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Shader
{
struct ShaderDisposeCommand : IGALCommand
{
public CommandType CommandType => CommandType.ShaderDispose;
private TableRef<ThreadedShader> _shader;
public void Set(TableRef<ThreadedShader> shader)
{
_shader = shader;
}
public static void Run(ref ShaderDisposeCommand command, ThreadedRenderer threaded, IRenderer renderer)
{
command._shader.Get(threaded).Base.Dispose();
}
}
}

View file

@ -6,10 +6,10 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Resources.Programs
{
public ThreadedProgram Threaded { get; set; }
private IShader[] _shaders;
private ShaderSource[] _shaders;
private ShaderInfo _info;
public SourceProgramRequest(ThreadedProgram program, IShader[] shaders, ShaderInfo info)
public SourceProgramRequest(ThreadedProgram program, ShaderSource[] shaders, ShaderInfo info)
{
Threaded = program;
@ -19,14 +19,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Resources.Programs
public IProgram Create(IRenderer renderer)
{
IShader[] shaders = _shaders.Select(shader =>
{
var threaded = (ThreadedShader)shader;
threaded?.EnsureCreated();
return threaded?.Base;
}).ToArray();
return renderer.CreateProgram(shaders, _info);
return renderer.CreateProgram(_shaders, _info);
}
}
}

View file

@ -1,38 +0,0 @@
using Ryujinx.Graphics.GAL.Multithreading.Commands.Shader;
using Ryujinx.Graphics.GAL.Multithreading.Model;
using Ryujinx.Graphics.Shader;
namespace Ryujinx.Graphics.GAL.Multithreading.Resources
{
class ThreadedShader : IShader
{
private ThreadedRenderer _renderer;
private ShaderStage _stage;
private string _code;
public IShader Base;
public ThreadedShader(ThreadedRenderer renderer, ShaderStage stage, string code)
{
_renderer = renderer;
_stage = stage;
_code = code;
}
internal void EnsureCreated()
{
if (_code != null && Base == null)
{
Base = _renderer.BaseRenderer.CompileShader(_stage, _code);
_code = null;
}
}
public void Dispose()
{
_renderer.New<ShaderDisposeCommand>().Set(new TableRef<ThreadedShader>(_renderer, this));
_renderer.QueueCommand();
}
}
}

View file

@ -1,7 +1,6 @@
using Ryujinx.Graphics.GAL.Multithreading.Commands;
using Ryujinx.Graphics.GAL.Multithreading.Model;
using Ryujinx.Graphics.GAL.Multithreading.Resources;
using Ryujinx.Graphics.Shader;
using System;
using System.Linq;

View file

@ -250,15 +250,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
}
}
public IShader CompileShader(ShaderStage stage, string code)
{
var shader = new ThreadedShader(this, stage, code);
New<CompileShaderCommand>().Set(Ref(shader));
QueueCommand();
return shader;
}
public BufferHandle CreateBuffer(int size)
{
BufferHandle handle = Buffers.CreateBufferHandle();
@ -268,7 +259,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
return handle;
}
public IProgram CreateProgram(IShader[] shaders, ShaderInfo info)
public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info)
{
var program = new ThreadedProgram(this);
SourceProgramRequest request = new SourceProgramRequest(program, shaders, info);

View file

@ -0,0 +1,29 @@
using Ryujinx.Graphics.Shader;
using Ryujinx.Graphics.Shader.Translation;
namespace Ryujinx.Graphics.GAL
{
public struct ShaderSource
{
public string Code { get; }
public byte[] BinaryCode { get; }
public ShaderStage Stage { get; }
public TargetLanguage Language { get; }
public ShaderSource(string code, byte[] binaryCode, ShaderStage stage, TargetLanguage language)
{
Code = code;
BinaryCode = binaryCode;
Stage = stage;
Language = language;
}
public ShaderSource(string code, ShaderStage stage, TargetLanguage language) : this(code, null, stage, language)
{
}
public ShaderSource(byte[] binaryCode, ShaderStage stage, TargetLanguage language) : this(null, binaryCode, stage, language)
{
}
}
}

View file

@ -124,24 +124,20 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute
ulong samplerPoolGpuVa = ((ulong)_state.State.SetTexSamplerPoolAOffsetUpper << 32) | _state.State.SetTexSamplerPoolB;
ulong texturePoolGpuVa = ((ulong)_state.State.SetTexHeaderPoolAOffsetUpper << 32) | _state.State.SetTexHeaderPoolB;
GpuAccessorState gas = new GpuAccessorState(
GpuChannelPoolState poolState = new GpuChannelPoolState(
texturePoolGpuVa,
_state.State.SetTexHeaderPoolCMaximumIndex,
_state.State.SetBindlessTextureConstantBufferSlotSelect,
false,
PrimitiveTopology.Points,
default);
_state.State.SetBindlessTextureConstantBufferSlotSelect);
ShaderBundle cs = memoryManager.Physical.ShaderCache.GetComputeShader(
_channel,
gas,
shaderGpuVa,
GpuChannelComputeState computeState = new GpuChannelComputeState(
qmd.CtaThreadDimension0,
qmd.CtaThreadDimension1,
qmd.CtaThreadDimension2,
localMemorySize,
sharedMemorySize);
CachedShaderProgram cs = memoryManager.Physical.ShaderCache.GetComputeShader(_channel, poolState, computeState, shaderGpuVa);
_context.Renderer.Pipeline.SetProgram(cs.HostProgram);
_channel.TextureManager.SetComputeSamplerPool(samplerPoolGpuVa, _state.State.SetTexSamplerPoolCMaximumIndex, qmd.SamplerIndex);

View file

@ -7,7 +7,6 @@ using Ryujinx.Graphics.Shader;
using Ryujinx.Graphics.Texture;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
@ -30,6 +29,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private readonly StateUpdateTracker<ThreedClassState> _updateTracker;
private readonly ShaderProgramInfo[] _currentProgramInfo;
private ShaderSpecializationState _shaderSpecState;
private bool _vtgWritesRtLayer;
private byte _vsClipDistancesWritten;
@ -195,6 +195,17 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Update()
{
// If any state that the shader depends on changed,
// then we may need to compile/bind a different version
// of the shader for the new state.
if (_shaderSpecState != null)
{
if (!_shaderSpecState.MatchesGraphics(_channel, GetPoolState()))
{
ForceShaderUpdate();
}
}
// The vertex buffer size is calculated using a different
// method when doing indexed draws, so we need to make sure
// to update the vertex buffers if we are doing a regular
@ -1065,108 +1076,127 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
/// </summary>
private void UpdateShaderState()
{
var shaderCache = _channel.MemoryManager.Physical.ShaderCache;
_vtgWritesRtLayer = false;
ShaderAddresses addresses = new ShaderAddresses();
Span<ShaderAddresses> addressesSpan = MemoryMarshal.CreateSpan(ref addresses, 1);
Span<ulong> addressesArray = MemoryMarshal.Cast<ShaderAddresses, ulong>(addressesSpan);
Span<ulong> addressesSpan = addresses.AsSpan();
ulong baseAddress = _state.State.ShaderBaseAddress.Pack();
for (int index = 0; index < 6; index++)
{
var shader = _state.State.ShaderState[index];
if (!shader.UnpackEnable() && index != 1)
{
continue;
}
addressesArray[index] = baseAddress + shader.Offset;
addressesSpan[index] = baseAddress + shader.Offset;
}
GpuAccessorState gas = new GpuAccessorState(
_state.State.TexturePoolState.Address.Pack(),
_state.State.TexturePoolState.MaximumId,
(int)_state.State.TextureBufferIndex,
_state.State.EarlyZForce,
_drawState.Topology,
_state.State.TessMode);
GpuChannelPoolState poolState = GetPoolState();
GpuChannelGraphicsState graphicsState = GetGraphicsState();
ShaderBundle gs = _channel.MemoryManager.Physical.ShaderCache.GetGraphicsShader(ref _state.State, _channel, gas, addresses);
CachedShaderProgram gs = shaderCache.GetGraphicsShader(ref _state.State, _channel, poolState, graphicsState, addresses);
_shaderSpecState = gs.SpecializationState;
byte oldVsClipDistancesWritten = _vsClipDistancesWritten;
_drawState.VsUsesInstanceId = gs.Shaders[0]?.Info.UsesInstanceId ?? false;
_vsClipDistancesWritten = gs.Shaders[0]?.Info.ClipDistancesWritten ?? 0;
_vtgWritesRtLayer = false;
_drawState.VsUsesInstanceId = gs.Shaders[1]?.Info.UsesInstanceId ?? false;
_vsClipDistancesWritten = gs.Shaders[1]?.Info.ClipDistancesWritten ?? 0;
if (oldVsClipDistancesWritten != _vsClipDistancesWritten)
{
UpdateUserClipState();
}
for (int stage = 0; stage < Constants.ShaderStages; stage++)
for (int stageIndex = 0; stageIndex < Constants.ShaderStages; stageIndex++)
{
ShaderProgramInfo info = gs.Shaders[stage]?.Info;
_currentProgramInfo[stage] = info;
if (info == null)
{
_channel.TextureManager.RentGraphicsTextureBindings(stage, 0);
_channel.TextureManager.RentGraphicsImageBindings(stage, 0);
_channel.BufferManager.SetGraphicsStorageBufferBindings(stage, null);
_channel.BufferManager.SetGraphicsUniformBufferBindings(stage, null);
continue;
}
Span<TextureBindingInfo> textureBindings = _channel.TextureManager.RentGraphicsTextureBindings(stage, info.Textures.Count);
if (info.UsesRtLayer)
{
_vtgWritesRtLayer = true;
}
for (int index = 0; index < info.Textures.Count; index++)
{
var descriptor = info.Textures[index];
Target target = ShaderTexture.GetTarget(descriptor.Type);
textureBindings[index] = new TextureBindingInfo(
target,
descriptor.Binding,
descriptor.CbufSlot,
descriptor.HandleIndex,
descriptor.Flags);
}
TextureBindingInfo[] imageBindings = _channel.TextureManager.RentGraphicsImageBindings(stage, info.Images.Count);
for (int index = 0; index < info.Images.Count; index++)
{
var descriptor = info.Images[index];
Target target = ShaderTexture.GetTarget(descriptor.Type);
Format format = ShaderTexture.GetFormat(descriptor.Format);
imageBindings[index] = new TextureBindingInfo(
target,
format,
descriptor.Binding,
descriptor.CbufSlot,
descriptor.HandleIndex,
descriptor.Flags);
}
_channel.BufferManager.SetGraphicsStorageBufferBindings(stage, info.SBuffers);
_channel.BufferManager.SetGraphicsUniformBufferBindings(stage, info.CBuffers);
UpdateStageBindings(stageIndex, gs.Shaders[stageIndex + 1]?.Info);
}
_context.Renderer.Pipeline.SetProgram(gs.HostProgram);
}
private void UpdateStageBindings(int stage, ShaderProgramInfo info)
{
_currentProgramInfo[stage] = info;
if (info == null)
{
_channel.TextureManager.RentGraphicsTextureBindings(stage, 0);
_channel.TextureManager.RentGraphicsImageBindings(stage, 0);
_channel.BufferManager.SetGraphicsStorageBufferBindings(stage, null);
_channel.BufferManager.SetGraphicsUniformBufferBindings(stage, null);
return;
}
Span<TextureBindingInfo> textureBindings = _channel.TextureManager.RentGraphicsTextureBindings(stage, info.Textures.Count);
if (info.UsesRtLayer)
{
_vtgWritesRtLayer = true;
}
for (int index = 0; index < info.Textures.Count; index++)
{
var descriptor = info.Textures[index];
Target target = ShaderTexture.GetTarget(descriptor.Type);
textureBindings[index] = new TextureBindingInfo(
target,
descriptor.Binding,
descriptor.CbufSlot,
descriptor.HandleIndex,
descriptor.Flags);
}
TextureBindingInfo[] imageBindings = _channel.TextureManager.RentGraphicsImageBindings(stage, info.Images.Count);
for (int index = 0; index < info.Images.Count; index++)
{
var descriptor = info.Images[index];
Target target = ShaderTexture.GetTarget(descriptor.Type);
Format format = ShaderTexture.GetFormat(descriptor.Format);
imageBindings[index] = new TextureBindingInfo(
target,
format,
descriptor.Binding,
descriptor.CbufSlot,
descriptor.HandleIndex,
descriptor.Flags);
}
_channel.BufferManager.SetGraphicsStorageBufferBindings(stage, info.SBuffers);
_channel.BufferManager.SetGraphicsUniformBufferBindings(stage, info.CBuffers);
}
private GpuChannelPoolState GetPoolState()
{
return new GpuChannelPoolState(
_state.State.TexturePoolState.Address.Pack(),
_state.State.TexturePoolState.MaximumId,
(int)_state.State.TextureBufferIndex);
}
/// <summary>
/// Gets the current GPU channel state for shader creation or compatibility verification.
/// </summary>
/// <returns>Current GPU channel state</returns>
private GpuChannelGraphicsState GetGraphicsState()
{
return new GpuChannelGraphicsState(
_state.State.EarlyZForce,
_drawState.Topology,
_state.State.TessMode);
}
/// <summary>
/// Forces the shaders to be rebound on the next draw.
/// </summary>

View file

@ -238,13 +238,13 @@ namespace Ryujinx.Graphics.Gpu
/// <summary>
/// Initialize the GPU shader cache.
/// </summary>
public void InitializeShaderCache()
public void InitializeShaderCache(CancellationToken cancellationToken)
{
HostInitalized.WaitOne();
foreach (var physicalMemory in PhysicalMemoryRegistry.Values)
{
physicalMemory.ShaderCache.Initialize();
physicalMemory.ShaderCache.Initialize(cancellationToken);
}
}

View file

@ -115,6 +115,73 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
/// <summary>
/// Gets a read-only span of data from GPU mapped memory, up to the entire range specified,
/// or the last mapped page if the range is not fully mapped.
/// </summary>
/// <param name="va">GPU virtual address where the data is located</param>
/// <param name="size">Size of the data</param>
/// <param name="tracked">True if read tracking is triggered on the span</param>
/// <returns>The span of the data at the specified memory location</returns>
public ReadOnlySpan<byte> GetSpanMapped(ulong va, int size, bool tracked = false)
{
bool isContiguous = true;
int mappedSize;
if (ValidateAddress(va) && GetPte(va) != PteUnmapped && Physical.IsMapped(Translate(va)))
{
ulong endVa = va + (ulong)size;
ulong endVaAligned = (endVa + PageMask) & ~PageMask;
ulong currentVa = va & ~PageMask;
int pages = (int)((endVaAligned - currentVa) / PageSize);
for (int page = 0; page < pages - 1; page++)
{
ulong nextVa = currentVa + PageSize;
ulong nextPa = Translate(nextVa);
if (!ValidateAddress(nextVa) || GetPte(nextVa) == PteUnmapped || !Physical.IsMapped(nextPa))
{
break;
}
if (Translate(currentVa) + PageSize != nextPa)
{
isContiguous = false;
}
currentVa += PageSize;
}
currentVa += PageSize;
if (currentVa > endVa)
{
currentVa = endVa;
}
mappedSize = (int)(currentVa - va);
}
else
{
return ReadOnlySpan<byte>.Empty;
}
if (isContiguous)
{
return Physical.GetSpan(Translate(va), mappedSize, tracked);
}
else
{
Span<byte> data = new byte[mappedSize];
ReadImpl(va, data, tracked);
return data;
}
}
/// <summary>
/// Reads data from a possibly non-contiguous region of GPU mapped memory.
/// </summary>

View file

@ -341,9 +341,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
/// <summary>
/// Checks if the page at a given address is mapped on CPU memory.
/// Checks if a given memory page is mapped.
/// </summary>
/// <param name="address">CPU virtual address of the page to check</param>
/// <param name="address">CPU virtual address of the page</param>
/// <returns>True if mapped, false otherwise</returns>
public bool IsMapped(ulong address)
{

View file

@ -2,11 +2,8 @@
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Memory;
using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
using Ryujinx.Graphics.Shader;
using Ryujinx.Graphics.Shader.Translation;
using System;
using System.Collections.Generic;
using System.IO;
@ -20,70 +17,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
/// </summary>
static class CacheHelper
{
/// <summary>
/// Try to read the manifest header from a given file path.
/// </summary>
/// <param name="manifestPath">The path to the manifest file</param>
/// <param name="header">The manifest header read</param>
/// <returns>Return true if the manifest header was read</returns>
public static bool TryReadManifestHeader(string manifestPath, out CacheManifestHeader header)
{
header = default;
if (File.Exists(manifestPath))
{
Memory<byte> rawManifest = File.ReadAllBytes(manifestPath);
if (MemoryMarshal.TryRead(rawManifest.Span, out header))
{
return true;
}
}
return false;
}
/// <summary>
/// Try to read the manifest from a given file path.
/// </summary>
/// <param name="manifestPath">The path to the manifest file</param>
/// <param name="graphicsApi">The graphics api used by the cache</param>
/// <param name="hashType">The hash type of the cache</param>
/// <param name="header">The manifest header read</param>
/// <param name="entries">The entries read from the cache manifest</param>
/// <returns>Return true if the manifest was read</returns>
public static bool TryReadManifestFile(string manifestPath, CacheGraphicsApi graphicsApi, CacheHashType hashType, out CacheManifestHeader header, out HashSet<Hash128> entries)
{
header = default;
entries = new HashSet<Hash128>();
if (File.Exists(manifestPath))
{
Memory<byte> rawManifest = File.ReadAllBytes(manifestPath);
if (MemoryMarshal.TryRead(rawManifest.Span, out header))
{
Memory<byte> hashTableRaw = rawManifest.Slice(Unsafe.SizeOf<CacheManifestHeader>());
bool isValid = header.IsValid(graphicsApi, hashType, hashTableRaw.Span);
if (isValid)
{
ReadOnlySpan<Hash128> hashTable = MemoryMarshal.Cast<byte, Hash128>(hashTableRaw.Span);
foreach (Hash128 hash in hashTable)
{
entries.Add(hash);
}
}
return isValid;
}
}
return false;
}
/// <summary>
/// Compute a cache manifest from runtime data.
/// </summary>
@ -246,82 +179,23 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
return null;
}
/// <summary>
/// Compute the guest program code for usage while dumping to disk or hash.
/// </summary>
/// <param name="cachedShaderEntries">The guest shader entries to use</param>
/// <param name="tfd">The transform feedback descriptors</param>
/// <param name="forHashCompute">Used to determine if the guest program code is generated for hashing</param>
/// <returns>The guest program code for usage while dumping to disk or hash</returns>
private static byte[] ComputeGuestProgramCode(ReadOnlySpan<GuestShaderCacheEntry> cachedShaderEntries, TransformFeedbackDescriptor[] tfd, bool forHashCompute = false)
{
using (MemoryStream stream = new MemoryStream())
{
BinaryWriter writer = new BinaryWriter(stream);
foreach (GuestShaderCacheEntry cachedShaderEntry in cachedShaderEntries)
{
if (cachedShaderEntry != null)
{
// Code (and Code A if present)
stream.Write(cachedShaderEntry.Code);
if (forHashCompute)
{
// Guest GPU accessor header (only write this for hashes, already present in the header for dumps)
writer.WriteStruct(cachedShaderEntry.Header.GpuAccessorHeader);
}
// Texture descriptors
foreach (GuestTextureDescriptor textureDescriptor in cachedShaderEntry.TextureDescriptors.Values)
{
writer.WriteStruct(textureDescriptor);
}
}
}
// Transform feedback
if (tfd != null)
{
foreach (TransformFeedbackDescriptor transform in tfd)
{
writer.WriteStruct(new GuestShaderCacheTransformFeedbackHeader(transform.BufferIndex, transform.Stride, transform.VaryingLocations.Length));
writer.Write(transform.VaryingLocations);
}
}
return stream.ToArray();
}
}
/// <summary>
/// Compute a guest hash from shader entries.
/// </summary>
/// <param name="cachedShaderEntries">The guest shader entries to use</param>
/// <param name="tfd">The optional transform feedback descriptors</param>
/// <returns>A guest hash from shader entries</returns>
public static Hash128 ComputeGuestHashFromCache(ReadOnlySpan<GuestShaderCacheEntry> cachedShaderEntries, TransformFeedbackDescriptor[] tfd = null)
{
return XXHash128.ComputeHash(ComputeGuestProgramCode(cachedShaderEntries, tfd, true));
}
/// <summary>
/// Read transform feedback descriptors from guest.
/// </summary>
/// <param name="data">The raw guest transform feedback descriptors</param>
/// <param name="header">The guest shader program header</param>
/// <returns>The transform feedback descriptors read from guest</returns>
public static TransformFeedbackDescriptor[] ReadTransformFeedbackInformation(ref ReadOnlySpan<byte> data, GuestShaderCacheHeader header)
public static TransformFeedbackDescriptorOld[] ReadTransformFeedbackInformation(ref ReadOnlySpan<byte> data, GuestShaderCacheHeader header)
{
if (header.TransformFeedbackCount != 0)
{
TransformFeedbackDescriptor[] result = new TransformFeedbackDescriptor[header.TransformFeedbackCount];
TransformFeedbackDescriptorOld[] result = new TransformFeedbackDescriptorOld[header.TransformFeedbackCount];
for (int i = 0; i < result.Length; i++)
{
GuestShaderCacheTransformFeedbackHeader feedbackHeader = MemoryMarshal.Read<GuestShaderCacheTransformFeedbackHeader>(data);
result[i] = new TransformFeedbackDescriptor(feedbackHeader.BufferIndex, feedbackHeader.Stride, data.Slice(Unsafe.SizeOf<GuestShaderCacheTransformFeedbackHeader>(), feedbackHeader.VaryingLocationsLength).ToArray());
result[i] = new TransformFeedbackDescriptorOld(feedbackHeader.BufferIndex, feedbackHeader.Stride, data.Slice(Unsafe.SizeOf<GuestShaderCacheTransformFeedbackHeader>(), feedbackHeader.VaryingLocationsLength).ToArray());
data = data.Slice(Unsafe.SizeOf<GuestShaderCacheTransformFeedbackHeader>() + feedbackHeader.VaryingLocationsLength);
}
@ -332,205 +206,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
return null;
}
/// <summary>
/// Builds gpu state flags using information from the given gpu accessor.
/// </summary>
/// <param name="gpuAccessor">The gpu accessor</param>
/// <returns>The gpu state flags</returns>
private static GuestGpuStateFlags GetGpuStateFlags(IGpuAccessor gpuAccessor)
{
GuestGpuStateFlags flags = 0;
if (gpuAccessor.QueryEarlyZForce())
{
flags |= GuestGpuStateFlags.EarlyZForce;
}
return flags;
}
/// <summary>
/// Packs the tessellation parameters from the gpu accessor.
/// </summary>
/// <param name="gpuAccessor">The gpu accessor</param>
/// <returns>The packed tessellation parameters</returns>
private static byte GetTessellationModePacked(IGpuAccessor gpuAccessor)
{
byte value;
value = (byte)((int)gpuAccessor.QueryTessPatchType() & 3);
value |= (byte)(((int)gpuAccessor.QueryTessSpacing() & 3) << 2);
if (gpuAccessor.QueryTessCw())
{
value |= 0x10;
}
return value;
}
/// <summary>
/// Create a new instance of <see cref="GuestGpuAccessorHeader"/> from an gpu accessor.
/// </summary>
/// <param name="gpuAccessor">The gpu accessor</param>
/// <returns>A new instance of <see cref="GuestGpuAccessorHeader"/></returns>
public static GuestGpuAccessorHeader CreateGuestGpuAccessorCache(IGpuAccessor gpuAccessor)
{
return new GuestGpuAccessorHeader
{
ComputeLocalSizeX = gpuAccessor.QueryComputeLocalSizeX(),
ComputeLocalSizeY = gpuAccessor.QueryComputeLocalSizeY(),
ComputeLocalSizeZ = gpuAccessor.QueryComputeLocalSizeZ(),
ComputeLocalMemorySize = gpuAccessor.QueryComputeLocalMemorySize(),
ComputeSharedMemorySize = gpuAccessor.QueryComputeSharedMemorySize(),
PrimitiveTopology = gpuAccessor.QueryPrimitiveTopology(),
TessellationModePacked = GetTessellationModePacked(gpuAccessor),
StateFlags = GetGpuStateFlags(gpuAccessor)
};
}
/// <summary>
/// Create guest shader cache entries from the runtime contexts.
/// </summary>
/// <param name="channel">The GPU channel in use</param>
/// <param name="shaderContexts">The runtime contexts</param>
/// <returns>Guest shader cahe entries from the runtime contexts</returns>
public static GuestShaderCacheEntry[] CreateShaderCacheEntries(GpuChannel channel, ReadOnlySpan<TranslatorContext> shaderContexts)
{
MemoryManager memoryManager = channel.MemoryManager;
int startIndex = shaderContexts.Length > 1 ? 1 : 0;
GuestShaderCacheEntry[] entries = new GuestShaderCacheEntry[shaderContexts.Length - startIndex];
for (int i = startIndex; i < shaderContexts.Length; i++)
{
TranslatorContext context = shaderContexts[i];
if (context == null)
{
continue;
}
GpuAccessor gpuAccessor = context.GpuAccessor as GpuAccessor;
ulong cb1DataAddress;
int cb1DataSize = gpuAccessor?.Cb1DataSize ?? 0;
if (context.Stage == ShaderStage.Compute)
{
cb1DataAddress = channel.BufferManager.GetComputeUniformBufferAddress(1);
}
else
{
int stageIndex = context.Stage switch
{
ShaderStage.TessellationControl => 1,
ShaderStage.TessellationEvaluation => 2,
ShaderStage.Geometry => 3,
ShaderStage.Fragment => 4,
_ => 0
};
cb1DataAddress = channel.BufferManager.GetGraphicsUniformBufferAddress(stageIndex, 1);
}
int size = context.Size;
TranslatorContext translatorContext2 = i == 1 ? shaderContexts[0] : null;
int sizeA = translatorContext2 != null ? translatorContext2.Size : 0;
byte[] code = new byte[size + cb1DataSize + sizeA];
memoryManager.GetSpan(context.Address, size).CopyTo(code);
if (cb1DataAddress != 0 && cb1DataSize != 0)
{
memoryManager.Physical.GetSpan(cb1DataAddress, cb1DataSize).CopyTo(code.AsSpan(size, cb1DataSize));
}
if (translatorContext2 != null)
{
memoryManager.GetSpan(translatorContext2.Address, sizeA).CopyTo(code.AsSpan(size + cb1DataSize, sizeA));
}
GuestGpuAccessorHeader gpuAccessorHeader = CreateGuestGpuAccessorCache(context.GpuAccessor);
if (gpuAccessor != null)
{
gpuAccessorHeader.TextureDescriptorCount = context.TextureHandlesForCache.Count;
}
GuestShaderCacheEntryHeader header = new GuestShaderCacheEntryHeader(
context.Stage,
size + cb1DataSize,
sizeA,
cb1DataSize,
gpuAccessorHeader);
GuestShaderCacheEntry entry = new GuestShaderCacheEntry(header, code);
if (gpuAccessor != null)
{
foreach (int textureHandle in context.TextureHandlesForCache)
{
GuestTextureDescriptor textureDescriptor = ((Image.TextureDescriptor)gpuAccessor.GetTextureDescriptor(textureHandle, -1)).ToCache();
textureDescriptor.Handle = (uint)textureHandle;
entry.TextureDescriptors.Add(textureHandle, textureDescriptor);
}
}
entries[i - startIndex] = entry;
}
return entries;
}
/// <summary>
/// Create a guest shader program.
/// </summary>
/// <param name="shaderCacheEntries">The entries composing the guest program dump</param>
/// <param name="tfd">The transform feedback descriptors in use</param>
/// <returns>The resulting guest shader program</returns>
public static byte[] CreateGuestProgramDump(GuestShaderCacheEntry[] shaderCacheEntries, TransformFeedbackDescriptor[] tfd = null)
{
using (MemoryStream resultStream = new MemoryStream())
{
BinaryWriter resultStreamWriter = new BinaryWriter(resultStream);
byte transformFeedbackCount = 0;
if (tfd != null)
{
transformFeedbackCount = (byte)tfd.Length;
}
// Header
resultStreamWriter.WriteStruct(new GuestShaderCacheHeader((byte)shaderCacheEntries.Length, transformFeedbackCount));
// Write all entries header
foreach (GuestShaderCacheEntry entry in shaderCacheEntries)
{
if (entry == null)
{
resultStreamWriter.WriteStruct(new GuestShaderCacheEntryHeader());
}
else
{
resultStreamWriter.WriteStruct(entry.Header);
}
}
// Finally, write all program code and all transform feedback information.
resultStreamWriter.Write(ComputeGuestProgramCode(shaderCacheEntries, tfd));
return resultStream.ToArray();
}
}
/// <summary>
/// Save temporary files not in archive.
/// </summary>

View file

@ -47,8 +47,6 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
string baseCacheDirectory = CacheHelper.GetBaseCacheDirectory(titleId);
CacheMigration.Run(baseCacheDirectory, graphicsApi, hashType, shaderProvider);
_guestProgramCache = new CacheCollection(baseCacheDirectory, _hashType, CacheGraphicsApi.Guest, "", "program", GuestCacheVersion);
_hostProgramCache = new CacheCollection(baseCacheDirectory, _hashType, _graphicsApi, _shaderProvider, "host", shaderCodeGenVersion);
}

View file

@ -1,175 +0,0 @@
using ICSharpCode.SharpZipLib.Zip;
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
using System;
using System.Collections.Generic;
using System.IO;
namespace Ryujinx.Graphics.Gpu.Shader.Cache
{
/// <summary>
/// Class handling shader cache migrations.
/// </summary>
static class CacheMigration
{
/// <summary>
/// Check if the given cache version need to recompute its hash.
/// </summary>
/// <param name="version">The version in use</param>
/// <param name="newVersion">The new version after migration</param>
/// <returns>True if a hash recompute is needed</returns>
public static bool NeedHashRecompute(ulong version, out ulong newVersion)
{
const ulong TargetBrokenVersion = 1717;
const ulong TargetFixedVersion = 1759;
newVersion = TargetFixedVersion;
if (version == TargetBrokenVersion)
{
return true;
}
return false;
}
private class StreamZipEntryDataSource : IStaticDataSource
{
private readonly ZipFile Archive;
private readonly ZipEntry Entry;
public StreamZipEntryDataSource(ZipFile archive, ZipEntry entry)
{
Archive = archive;
Entry = entry;
}
public Stream GetSource()
{
return Archive.GetInputStream(Entry);
}
}
/// <summary>
/// Move a file with the name of a given hash to another in the cache archive.
/// </summary>
/// <param name="archive">The archive in use</param>
/// <param name="oldKey">The old key</param>
/// <param name="newKey">The new key</param>
private static void MoveEntry(ZipFile archive, Hash128 oldKey, Hash128 newKey)
{
ZipEntry oldGuestEntry = archive.GetEntry($"{oldKey}");
if (oldGuestEntry != null)
{
archive.Add(new StreamZipEntryDataSource(archive, oldGuestEntry), $"{newKey}", CompressionMethod.Deflated);
archive.Delete(oldGuestEntry);
}
}
/// <summary>
/// Recompute all the hashes of a given cache.
/// </summary>
/// <param name="guestBaseCacheDirectory">The guest cache directory path</param>
/// <param name="hostBaseCacheDirectory">The host cache directory path</param>
/// <param name="graphicsApi">The graphics api in use</param>
/// <param name="hashType">The hash type in use</param>
/// <param name="newVersion">The version to write in the host and guest manifest after migration</param>
private static void RecomputeHashes(string guestBaseCacheDirectory, string hostBaseCacheDirectory, CacheGraphicsApi graphicsApi, CacheHashType hashType, ulong newVersion)
{
string guestManifestPath = CacheHelper.GetManifestPath(guestBaseCacheDirectory);
string hostManifestPath = CacheHelper.GetManifestPath(hostBaseCacheDirectory);
if (CacheHelper.TryReadManifestFile(guestManifestPath, CacheGraphicsApi.Guest, hashType, out _, out HashSet<Hash128> guestEntries))
{
CacheHelper.TryReadManifestFile(hostManifestPath, graphicsApi, hashType, out _, out HashSet<Hash128> hostEntries);
Logger.Info?.Print(LogClass.Gpu, "Shader cache hashes need to be recomputed, performing migration...");
string guestArchivePath = CacheHelper.GetArchivePath(guestBaseCacheDirectory);
string hostArchivePath = CacheHelper.GetArchivePath(hostBaseCacheDirectory);
ZipFile guestArchive = new ZipFile(File.Open(guestArchivePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None));
ZipFile hostArchive = new ZipFile(File.Open(hostArchivePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None));
CacheHelper.EnsureArchiveUpToDate(guestBaseCacheDirectory, guestArchive, guestEntries);
CacheHelper.EnsureArchiveUpToDate(hostBaseCacheDirectory, hostArchive, hostEntries);
int programIndex = 0;
HashSet<Hash128> newEntries = new HashSet<Hash128>();
foreach (Hash128 oldHash in guestEntries)
{
byte[] guestProgram = CacheHelper.ReadFromArchive(guestArchive, oldHash);
Logger.Info?.Print(LogClass.Gpu, $"Migrating shader {oldHash} ({programIndex + 1} / {guestEntries.Count})");
if (guestProgram != null)
{
ReadOnlySpan<byte> guestProgramReadOnlySpan = guestProgram;
ReadOnlySpan<GuestShaderCacheEntry> cachedShaderEntries = GuestShaderCacheEntry.Parse(ref guestProgramReadOnlySpan, out GuestShaderCacheHeader fileHeader);
TransformFeedbackDescriptor[] tfd = CacheHelper.ReadTransformFeedbackInformation(ref guestProgramReadOnlySpan, fileHeader);
Hash128 newHash = CacheHelper.ComputeGuestHashFromCache(cachedShaderEntries, tfd);
if (newHash != oldHash)
{
MoveEntry(guestArchive, oldHash, newHash);
MoveEntry(hostArchive, oldHash, newHash);
}
else
{
Logger.Warning?.Print(LogClass.Gpu, $"Same hashes for shader {oldHash}");
}
newEntries.Add(newHash);
}
programIndex++;
}
byte[] newGuestManifestContent = CacheHelper.ComputeManifest(newVersion, CacheGraphicsApi.Guest, hashType, newEntries);
byte[] newHostManifestContent = CacheHelper.ComputeManifest(newVersion, graphicsApi, hashType, newEntries);
File.WriteAllBytes(guestManifestPath, newGuestManifestContent);
File.WriteAllBytes(hostManifestPath, newHostManifestContent);
guestArchive.CommitUpdate();
hostArchive.CommitUpdate();
guestArchive.Close();
hostArchive.Close();
}
}
/// <summary>
/// Check and run cache migration if needed.
/// </summary>
/// <param name="baseCacheDirectory">The base path of the cache</param>
/// <param name="graphicsApi">The graphics api in use</param>
/// <param name="hashType">The hash type in use</param>
/// <param name="shaderProvider">The shader provider name of the cache</param>
public static void Run(string baseCacheDirectory, CacheGraphicsApi graphicsApi, CacheHashType hashType, string shaderProvider)
{
string guestBaseCacheDirectory = CacheHelper.GenerateCachePath(baseCacheDirectory, CacheGraphicsApi.Guest, "", "program");
string hostBaseCacheDirectory = CacheHelper.GenerateCachePath(baseCacheDirectory, graphicsApi, shaderProvider, "host");
string guestArchivePath = CacheHelper.GetArchivePath(guestBaseCacheDirectory);
string hostArchivePath = CacheHelper.GetArchivePath(hostBaseCacheDirectory);
bool isReadOnly = CacheHelper.IsArchiveReadOnly(guestArchivePath) || CacheHelper.IsArchiveReadOnly(hostArchivePath);
if (!isReadOnly && CacheHelper.TryReadManifestHeader(CacheHelper.GetManifestPath(guestBaseCacheDirectory), out CacheManifestHeader header))
{
if (NeedHashRecompute(header.Version, out ulong newVersion))
{
RecomputeHashes(guestBaseCacheDirectory, hostBaseCacheDirectory, graphicsApi, hashType, newVersion);
}
}
}
}
}

View file

@ -96,6 +96,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache.Definition
SBuffers,
Textures,
Images,
default,
Header.UseFlags.HasFlag(UseFlags.InstanceId),
Header.UseFlags.HasFlag(UseFlags.RtLayer),
Header.ClipDistancesWritten,
@ -160,7 +161,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache.Definition
/// <param name="programCode">The host shader program</param>
/// <param name="codeHolders">The shaders code holder</param>
/// <returns>Raw data of a new host shader cache file</returns>
internal static byte[] Create(ReadOnlySpan<byte> programCode, ShaderCodeHolder[] codeHolders)
internal static byte[] Create(ReadOnlySpan<byte> programCode, CachedShaderStage[] codeHolders)
{
HostShaderCacheHeader header = new HostShaderCacheHeader((byte)codeHolders.Length, programCode.Length);

View file

@ -0,0 +1,255 @@
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Engine.Threed;
using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
using Ryujinx.Graphics.Gpu.Shader.DiskCache;
using Ryujinx.Graphics.Shader;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Shader.Cache
{
/// <summary>
/// Class handling shader cache migrations.
/// </summary>
static class Migration
{
// Last codegen version before the migration to the new cache.
private const ulong ShaderCodeGenVersion = 3054;
/// <summary>
/// Migrates from the old cache format to the new one.
/// </summary>
/// <param name="context">GPU context</param>
/// <param name="hostStorage">Disk cache host storage (used to create the new shader files)</param>
/// <returns>Number of migrated shaders</returns>
public static int MigrateFromLegacyCache(GpuContext context, DiskCacheHostStorage hostStorage)
{
string baseCacheDirectory = CacheHelper.GetBaseCacheDirectory(GraphicsConfig.TitleId);
string cacheDirectory = CacheHelper.GenerateCachePath(baseCacheDirectory, CacheGraphicsApi.Guest, "", "program");
// If the directory does not exist, we have no old cache.
// Exist early as the CacheManager constructor will create the directories.
if (!Directory.Exists(cacheDirectory))
{
return 0;
}
if (GraphicsConfig.EnableShaderCache && GraphicsConfig.TitleId != null)
{
CacheManager cacheManager = new CacheManager(CacheGraphicsApi.OpenGL, CacheHashType.XxHash128, "glsl", GraphicsConfig.TitleId, ShaderCodeGenVersion);
bool isReadOnly = cacheManager.IsReadOnly;
HashSet<Hash128> invalidEntries = null;
if (isReadOnly)
{
Logger.Warning?.Print(LogClass.Gpu, "Loading shader cache in read-only mode (cache in use by another program!)");
}
else
{
invalidEntries = new HashSet<Hash128>();
}
ReadOnlySpan<Hash128> guestProgramList = cacheManager.GetGuestProgramList();
for (int programIndex = 0; programIndex < guestProgramList.Length; programIndex++)
{
Hash128 key = guestProgramList[programIndex];
byte[] guestProgram = cacheManager.GetGuestProgramByHash(ref key);
if (guestProgram == null)
{
Logger.Error?.Print(LogClass.Gpu, $"Ignoring orphan shader hash {key} in cache (is the cache incomplete?)");
continue;
}
ReadOnlySpan<byte> guestProgramReadOnlySpan = guestProgram;
ReadOnlySpan<GuestShaderCacheEntry> cachedShaderEntries = GuestShaderCacheEntry.Parse(ref guestProgramReadOnlySpan, out GuestShaderCacheHeader fileHeader);
if (cachedShaderEntries[0].Header.Stage == ShaderStage.Compute)
{
Debug.Assert(cachedShaderEntries.Length == 1);
GuestShaderCacheEntry entry = cachedShaderEntries[0];
byte[] code = entry.Code.AsSpan(0, entry.Header.Size - entry.Header.Cb1DataSize).ToArray();
Span<byte> codeSpan = entry.Code;
byte[] cb1Data = codeSpan.Slice(codeSpan.Length - entry.Header.Cb1DataSize).ToArray();
ShaderProgramInfo info = new ShaderProgramInfo(
Array.Empty<BufferDescriptor>(),
Array.Empty<BufferDescriptor>(),
Array.Empty<TextureDescriptor>(),
Array.Empty<TextureDescriptor>(),
ShaderStage.Compute,
false,
false,
0,
0);
GpuChannelComputeState computeState = new GpuChannelComputeState(
entry.Header.GpuAccessorHeader.ComputeLocalSizeX,
entry.Header.GpuAccessorHeader.ComputeLocalSizeY,
entry.Header.GpuAccessorHeader.ComputeLocalSizeZ,
entry.Header.GpuAccessorHeader.ComputeLocalMemorySize,
entry.Header.GpuAccessorHeader.ComputeSharedMemorySize);
ShaderSpecializationState specState = new ShaderSpecializationState(computeState);
foreach (var td in entry.TextureDescriptors)
{
var handle = td.Key;
var data = td.Value;
specState.RegisterTexture(
0,
handle,
-1,
data.UnpackFormat(),
data.UnpackSrgb(),
data.UnpackTextureTarget(),
data.UnpackTextureCoordNormalized());
}
CachedShaderStage shader = new CachedShaderStage(info, code, cb1Data);
CachedShaderProgram program = new CachedShaderProgram(null, specState, shader);
hostStorage.AddShader(context, program, ReadOnlySpan<byte>.Empty);
}
else
{
Debug.Assert(cachedShaderEntries.Length == Constants.ShaderStages);
CachedShaderStage[] shaders = new CachedShaderStage[Constants.ShaderStages + 1];
List<ShaderProgram> shaderPrograms = new List<ShaderProgram>();
TransformFeedbackDescriptorOld[] tfd = CacheHelper.ReadTransformFeedbackInformation(ref guestProgramReadOnlySpan, fileHeader);
GuestShaderCacheEntry[] entries = cachedShaderEntries.ToArray();
GuestGpuAccessorHeader accessorHeader = entries[0].Header.GpuAccessorHeader;
TessMode tessMode = new TessMode();
int tessPatchType = accessorHeader.TessellationModePacked & 3;
int tessSpacing = (accessorHeader.TessellationModePacked >> 2) & 3;
bool tessCw = (accessorHeader.TessellationModePacked & 0x10) != 0;
tessMode.Packed = (uint)tessPatchType;
tessMode.Packed |= (uint)(tessSpacing << 4);
if (tessCw)
{
tessMode.Packed |= 0x100;
}
PrimitiveTopology topology = accessorHeader.PrimitiveTopology switch
{
InputTopology.Lines => PrimitiveTopology.Lines,
InputTopology.LinesAdjacency => PrimitiveTopology.LinesAdjacency,
InputTopology.Triangles => PrimitiveTopology.Triangles,
InputTopology.TrianglesAdjacency => PrimitiveTopology.TrianglesAdjacency,
_ => PrimitiveTopology.Points
};
GpuChannelGraphicsState graphicsState = new GpuChannelGraphicsState(
accessorHeader.StateFlags.HasFlag(GuestGpuStateFlags.EarlyZForce),
topology,
tessMode);
TransformFeedbackDescriptor[] tfdNew = null;
if (tfd != null)
{
tfdNew = new TransformFeedbackDescriptor[tfd.Length];
for (int tfIndex = 0; tfIndex < tfd.Length; tfIndex++)
{
Array32<uint> varyingLocations = new Array32<uint>();
Span<byte> varyingLocationsSpan = MemoryMarshal.Cast<uint, byte>(varyingLocations.ToSpan());
tfd[tfIndex].VaryingLocations.CopyTo(varyingLocationsSpan.Slice(0, tfd[tfIndex].VaryingLocations.Length));
tfdNew[tfIndex] = new TransformFeedbackDescriptor(
tfd[tfIndex].BufferIndex,
tfd[tfIndex].Stride,
tfd[tfIndex].VaryingLocations.Length,
ref varyingLocations);
}
}
ShaderSpecializationState specState = new ShaderSpecializationState(graphicsState, tfdNew);
for (int i = 0; i < entries.Length; i++)
{
GuestShaderCacheEntry entry = entries[i];
if (entry == null)
{
continue;
}
ShaderProgramInfo info = new ShaderProgramInfo(
Array.Empty<BufferDescriptor>(),
Array.Empty<BufferDescriptor>(),
Array.Empty<TextureDescriptor>(),
Array.Empty<TextureDescriptor>(),
(ShaderStage)(i + 1),
false,
false,
0,
0);
// NOTE: Vertex B comes first in the shader cache.
byte[] code = entry.Code.AsSpan(0, entry.Header.Size - entry.Header.Cb1DataSize).ToArray();
byte[] code2 = entry.Header.SizeA != 0 ? entry.Code.AsSpan(entry.Header.Size, entry.Header.SizeA).ToArray() : null;
Span<byte> codeSpan = entry.Code;
byte[] cb1Data = codeSpan.Slice(codeSpan.Length - entry.Header.Cb1DataSize).ToArray();
shaders[i + 1] = new CachedShaderStage(info, code, cb1Data);
if (code2 != null)
{
shaders[0] = new CachedShaderStage(null, code2, cb1Data);
}
foreach (var td in entry.TextureDescriptors)
{
var handle = td.Key;
var data = td.Value;
specState.RegisterTexture(
i,
handle,
-1,
data.UnpackFormat(),
data.UnpackSrgb(),
data.UnpackTextureTarget(),
data.UnpackTextureCoordNormalized());
}
}
CachedShaderProgram program = new CachedShaderProgram(null, specState, shaders);
hostStorage.AddShader(context, program, ReadOnlySpan<byte>.Empty);
}
}
return guestProgramList.Length;
}
return 0;
}
}
}

View file

@ -0,0 +1,19 @@
using System;
namespace Ryujinx.Graphics.Gpu.Shader.Cache
{
struct TransformFeedbackDescriptorOld
{
public int BufferIndex { get; }
public int Stride { get; }
public byte[] VaryingLocations { get; }
public TransformFeedbackDescriptorOld(int bufferIndex, int stride, byte[] varyingLocations)
{
BufferIndex = bufferIndex;
Stride = stride;
VaryingLocations = varyingLocations ?? throw new ArgumentNullException(nameof(varyingLocations));
}
}
}

View file

@ -1,222 +0,0 @@
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
using Ryujinx.Graphics.Shader;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Shader
{
class CachedGpuAccessor : TextureDescriptorCapableGpuAccessor, IGpuAccessor
{
private readonly ReadOnlyMemory<byte> _data;
private readonly ReadOnlyMemory<byte> _cb1Data;
private readonly GuestGpuAccessorHeader _header;
private readonly Dictionary<int, GuestTextureDescriptor> _textureDescriptors;
private readonly TransformFeedbackDescriptor[] _tfd;
/// <summary>
/// Creates a new instance of the cached GPU state accessor for shader translation.
/// </summary>
/// <param name="context">GPU context</param>
/// <param name="data">The data of the shader</param>
/// <param name="cb1Data">The constant buffer 1 data of the shader</param>
/// <param name="header">The cache of the GPU accessor</param>
/// <param name="guestTextureDescriptors">The cache of the texture descriptors</param>
public CachedGpuAccessor(
GpuContext context,
ReadOnlyMemory<byte> data,
ReadOnlyMemory<byte> cb1Data,
GuestGpuAccessorHeader header,
IReadOnlyDictionary<int, GuestTextureDescriptor> guestTextureDescriptors,
TransformFeedbackDescriptor[] tfd) : base(context)
{
_data = data;
_cb1Data = cb1Data;
_header = header;
_textureDescriptors = new Dictionary<int, GuestTextureDescriptor>();
foreach (KeyValuePair<int, GuestTextureDescriptor> guestTextureDescriptor in guestTextureDescriptors)
{
_textureDescriptors.Add(guestTextureDescriptor.Key, guestTextureDescriptor.Value);
}
_tfd = tfd;
}
/// <summary>
/// Reads data from the constant buffer 1.
/// </summary>
/// <param name="offset">Offset in bytes to read from</param>
/// <returns>Value at the given offset</returns>
public uint ConstantBuffer1Read(int offset)
{
return MemoryMarshal.Cast<byte, uint>(_cb1Data.Span.Slice(offset))[0];
}
/// <summary>
/// Prints a log message.
/// </summary>
/// <param name="message">Message to print</param>
public void Log(string message)
{
Logger.Warning?.Print(LogClass.Gpu, $"Shader translator: {message}");
}
/// <summary>
/// Gets a span of the specified memory location, containing shader code.
/// </summary>
/// <param name="address">GPU virtual address of the data</param>
/// <param name="minimumSize">Minimum size that the returned span may have</param>
/// <returns>Span of the memory location</returns>
public override ReadOnlySpan<ulong> GetCode(ulong address, int minimumSize)
{
return MemoryMarshal.Cast<byte, ulong>(_data.Span.Slice((int)address));
}
/// <summary>
/// Checks if a given memory address is mapped.
/// </summary>
/// <param name="address">GPU virtual address to be checked</param>
/// <returns>True if the address is mapped, false otherwise</returns>
public bool MemoryMapped(ulong address)
{
return address < (ulong)_data.Length;
}
/// <summary>
/// Queries Local Size X for compute shaders.
/// </summary>
/// <returns>Local Size X</returns>
public int QueryComputeLocalSizeX()
{
return _header.ComputeLocalSizeX;
}
/// <summary>
/// Queries Local Size Y for compute shaders.
/// </summary>
/// <returns>Local Size Y</returns>
public int QueryComputeLocalSizeY()
{
return _header.ComputeLocalSizeY;
}
/// <summary>
/// Queries Local Size Z for compute shaders.
/// </summary>
/// <returns>Local Size Z</returns>
public int QueryComputeLocalSizeZ()
{
return _header.ComputeLocalSizeZ;
}
/// <summary>
/// Queries Local Memory size in bytes for compute shaders.
/// </summary>
/// <returns>Local Memory size in bytes</returns>
public int QueryComputeLocalMemorySize()
{
return _header.ComputeLocalMemorySize;
}
/// <summary>
/// Queries Shared Memory size in bytes for compute shaders.
/// </summary>
/// <returns>Shared Memory size in bytes</returns>
public int QueryComputeSharedMemorySize()
{
return _header.ComputeSharedMemorySize;
}
/// <summary>
/// Queries current primitive topology for geometry shaders.
/// </summary>
/// <returns>Current primitive topology</returns>
public InputTopology QueryPrimitiveTopology()
{
return _header.PrimitiveTopology;
}
/// <summary>
/// Queries the tessellation evaluation shader primitive winding order.
/// </summary>
/// <returns>True if the primitive winding order is clockwise, false if counter-clockwise</returns>
public bool QueryTessCw()
{
return (_header.TessellationModePacked & 0x10) != 0;
}
/// <summary>
/// Queries the tessellation evaluation shader abstract patch type.
/// </summary>
/// <returns>Abstract patch type</returns>
public TessPatchType QueryTessPatchType()
{
return (TessPatchType)(_header.TessellationModePacked & 3);
}
/// <summary>
/// Queries the tessellation evaluation shader spacing between tessellated vertices of the patch.
/// </summary>
/// <returns>Spacing between tessellated vertices of the patch</returns>
public TessSpacing QueryTessSpacing()
{
return (TessSpacing)((_header.TessellationModePacked >> 2) & 3);
}
/// <summary>
/// Gets the texture descriptor for a given texture on the pool.
/// </summary>
/// <param name="handle">Index of the texture (this is the word offset of the handle in the constant buffer)</param>
/// <param name="cbufSlot">Constant buffer slot for the texture handle</param>
/// <returns>Texture descriptor</returns>
public override Image.ITextureDescriptor GetTextureDescriptor(int handle, int cbufSlot)
{
if (!_textureDescriptors.TryGetValue(handle, out GuestTextureDescriptor textureDescriptor))
{
throw new ArgumentException();
}
return textureDescriptor;
}
/// <summary>
/// Queries transform feedback enable state.
/// </summary>
/// <returns>True if the shader uses transform feedback, false otherwise</returns>
public bool QueryTransformFeedbackEnabled()
{
return _tfd != null;
}
/// <summary>
/// Queries the varying locations that should be written to the transform feedback buffer.
/// </summary>
/// <param name="bufferIndex">Index of the transform feedback buffer</param>
/// <returns>Varying locations for the specified buffer</returns>
public ReadOnlySpan<byte> QueryTransformFeedbackVaryingLocations(int bufferIndex)
{
return _tfd[bufferIndex].VaryingLocations;
}
/// <summary>
/// Queries the stride (in bytes) of the per vertex data written into the transform feedback buffer.
/// </summary>
/// <param name="bufferIndex">Index of the transform feedback buffer</param>
/// <returns>Stride for the specified buffer</returns>
public int QueryTransformFeedbackStride(int bufferIndex)
{
return _tfd[bufferIndex].Stride;
}
/// <summary>
/// Queries if host state forces early depth testing.
/// </summary>
/// <returns>True if early depth testing is forced</returns>
public bool QueryEarlyZForce()
{
return (_header.StateFlags & GuestGpuStateFlags.EarlyZForce) != 0;
}
}
}

View file

@ -7,26 +7,33 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// Represents a program composed of one or more shader stages (for graphics shaders),
/// or a single shader (for compute shaders).
/// </summary>
class ShaderBundle : IDisposable
class CachedShaderProgram : IDisposable
{
/// <summary>
/// Host shader program object.
/// </summary>
public IProgram HostProgram { get; }
/// <summary>
/// GPU state used to create this version of the shader.
/// </summary>
public ShaderSpecializationState SpecializationState { get; }
/// <summary>
/// Compiled shader for each shader stage.
/// </summary>
public ShaderCodeHolder[] Shaders { get; }
public CachedShaderStage[] Shaders { get; }
/// <summary>
/// Creates a new instance of the shader bundle.
/// </summary>
/// <param name="hostProgram">Host program with all the shader stages</param>
/// <param name="specializationState">GPU state used to create this version of the shader</param>
/// <param name="shaders">Shaders</param>
public ShaderBundle(IProgram hostProgram, params ShaderCodeHolder[] shaders)
public CachedShaderProgram(IProgram hostProgram, ShaderSpecializationState specializationState, params CachedShaderStage[] shaders)
{
HostProgram = hostProgram;
SpecializationState = specializationState;
Shaders = shaders;
}
@ -36,11 +43,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
public void Dispose()
{
HostProgram.Dispose();
foreach (ShaderCodeHolder holder in Shaders)
{
holder?.HostShader?.Dispose();
}
}
}
}

View file

@ -0,0 +1,38 @@
using Ryujinx.Graphics.Shader;
namespace Ryujinx.Graphics.Gpu.Shader
{
/// <summary>
/// Cached shader code for a single shader stage.
/// </summary>
class CachedShaderStage
{
/// <summary>
/// Shader program information.
/// </summary>
public ShaderProgramInfo Info { get; }
/// <summary>
/// Maxwell binary shader code.
/// </summary>
public byte[] Code { get; }
/// <summary>
/// Constant buffer 1 data accessed by the shader.
/// </summary>
public byte[] Cb1Data { get; }
/// <summary>
/// Creates a new instance of the shader code holder.
/// </summary>
/// <param name="info">Shader program information</param>
/// <param name="code">Maxwell binary shader code</param>
/// <param name="cb1Data">Constant buffer 1 data accessed by the shader</param>
public CachedShaderStage(ShaderProgramInfo info, byte[] code, byte[] cb1Data)
{
Info = info;
Code = code;
Cb1Data = cb1Data;
}
}
}

View file

@ -0,0 +1,68 @@
using Ryujinx.Graphics.Gpu.Shader.HashTable;
using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Shader
{
/// <summary>
/// Compute shader cache hash table.
/// </summary>
class ComputeShaderCacheHashTable
{
private readonly PartitionedHashTable<ShaderSpecializationList> _cache;
private readonly List<CachedShaderProgram> _shaderPrograms;
/// <summary>
/// Creates a new compute shader cache hash table.
/// </summary>
public ComputeShaderCacheHashTable()
{
_cache = new PartitionedHashTable<ShaderSpecializationList>();
_shaderPrograms = new List<CachedShaderProgram>();
}
/// <summary>
/// Adds a program to the cache.
/// </summary>
/// <param name="program">Program to be added</param>
public void Add(CachedShaderProgram program)
{
var specList = _cache.GetOrAdd(program.Shaders[0].Code, new ShaderSpecializationList());
specList.Add(program);
_shaderPrograms.Add(program);
}
/// <summary>
/// Tries to find a cached program.
/// </summary>
/// <param name="channel">GPU channel</param>
/// <param name="poolState">Texture pool state</param>
/// <param name="gpuVa">GPU virtual address of the compute shader</param>
/// <param name="program">Cached host program for the given state, if found</param>
/// <param name="cachedGuestCode">Cached guest code, if any found</param>
/// <returns>True if a cached host program was found, false otherwise</returns>
public bool TryFind(
GpuChannel channel,
GpuChannelPoolState poolState,
ulong gpuVa,
out CachedShaderProgram program,
out byte[] cachedGuestCode)
{
program = null;
ShaderCodeAccessor codeAccessor = new ShaderCodeAccessor(channel.MemoryManager, gpuVa);
bool hasSpecList = _cache.TryFindItem(codeAccessor, out var specList, out cachedGuestCode);
return hasSpecList && specList.TryFindForCompute(channel, poolState, out program);
}
/// <summary>
/// Gets all programs that have been added to the table.
/// </summary>
/// <returns>Programs added to the table</returns>
public IEnumerable<CachedShaderProgram> GetPrograms()
{
foreach (var program in _shaderPrograms)
{
yield return program;
}
}
}
}

View file

@ -0,0 +1,138 @@
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using System;
using System.IO;
namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
{
/// <summary>
/// Represents a background disk cache writer.
/// </summary>
class BackgroundDiskCacheWriter : IDisposable
{
/// <summary>
/// Possible operation to do on the <see cref="_fileWriterWorkerQueue"/>.
/// </summary>
private enum CacheFileOperation
{
/// <summary>
/// Operation to add a shader to the cache.
/// </summary>
AddShader
}
/// <summary>
/// Represents an operation to perform on the <see cref="_fileWriterWorkerQueue"/>.
/// </summary>
private struct CacheFileOperationTask
{
/// <summary>
/// The type of operation to perform.
/// </summary>
public readonly CacheFileOperation Type;
/// <summary>
/// The data associated to this operation or null.
/// </summary>
public readonly object Data;
public CacheFileOperationTask(CacheFileOperation type, object data)
{
Type = type;
Data = data;
}
}
/// <summary>
/// Background shader cache write information.
/// </summary>
private struct AddShaderData
{
/// <summary>
/// Cached shader program.
/// </summary>
public readonly CachedShaderProgram Program;
/// <summary>
/// Binary host code.
/// </summary>
public readonly byte[] HostCode;
/// <summary>
/// Creates a new background shader cache write information.
/// </summary>
/// <param name="program">Cached shader program</param>
/// <param name="hostCode">Binary host code</param>
public AddShaderData(CachedShaderProgram program, byte[] hostCode)
{
Program = program;
HostCode = hostCode;
}
}
private readonly GpuContext _context;
private readonly DiskCacheHostStorage _hostStorage;
private readonly AsyncWorkQueue<CacheFileOperationTask> _fileWriterWorkerQueue;
/// <summary>
/// Creates a new background disk cache writer.
/// </summary>
/// <param name="context">GPU context</param>
/// <param name="hostStorage">Disk cache host storage</param>
public BackgroundDiskCacheWriter(GpuContext context, DiskCacheHostStorage hostStorage)
{
_context = context;
_hostStorage = hostStorage;
_fileWriterWorkerQueue = new AsyncWorkQueue<CacheFileOperationTask>(ProcessTask, "Gpu.BackgroundDiskCacheWriter");
}
/// <summary>
/// Processes a shader cache background operation.
/// </summary>
/// <param name="task">Task to process</param>
private void ProcessTask(CacheFileOperationTask task)
{
switch (task.Type)
{
case CacheFileOperation.AddShader:
AddShaderData data = (AddShaderData)task.Data;
try
{
_hostStorage.AddShader(_context, data.Program, data.HostCode);
}
catch (DiskCacheLoadException diskCacheLoadException)
{
Logger.Error?.Print(LogClass.Gpu, $"Error writing shader to disk cache. {diskCacheLoadException.Message}");
}
catch (IOException ioException)
{
Logger.Error?.Print(LogClass.Gpu, $"Error writing shader to disk cache. {ioException.Message}");
}
break;
}
}
/// <summary>
/// Adds a shader program to be cached in the background.
/// </summary>
/// <param name="program">Shader program to cache</param>
/// <param name="hostCode">Host binary code of the program</param>
public void AddShader(CachedShaderProgram program, byte[] hostCode)
{
_fileWriterWorkerQueue.Add(new CacheFileOperationTask(CacheFileOperation.AddShader, new AddShaderData(program, hostCode)));
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_fileWriterWorkerQueue.Dispose();
}
}
}
}

View file

@ -0,0 +1,216 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
{
/// <summary>
/// Binary data serializer.
/// </summary>
struct BinarySerializer
{
private readonly Stream _stream;
private Stream _activeStream;
/// <summary>
/// Creates a new binary serializer.
/// </summary>
/// <param name="stream">Stream to read from or write into</param>
public BinarySerializer(Stream stream)
{
_stream = stream;
_activeStream = stream;
}
/// <summary>
/// Reads data from the stream.
/// </summary>
/// <typeparam name="T">Type of the data</typeparam>
/// <param name="data">Data read</param>
public void Read<T>(ref T data) where T : unmanaged
{
Span<byte> buffer = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref data, 1));
for (int offset = 0; offset < buffer.Length;)
{
offset += _activeStream.Read(buffer.Slice(offset));
}
}
/// <summary>
/// Tries to read data from the stream.
/// </summary>
/// <typeparam name="T">Type of the data</typeparam>
/// <param name="data">Data read</param>
/// <returns>True if the read was successful, false otherwise</returns>
public bool TryRead<T>(ref T data) where T : unmanaged
{
// Length is unknown on compressed streams.
if (_activeStream == _stream)
{
int size = Unsafe.SizeOf<T>();
if (_activeStream.Length - _activeStream.Position < size)
{
return false;
}
}
Read(ref data);
return true;
}
/// <summary>
/// Reads data prefixed with a magic and size from the stream.
/// </summary>
/// <typeparam name="T">Type of the data</typeparam>
/// <param name="data">Data read</param>
/// <param name="magic">Expected magic value, for validation</param>
public void ReadWithMagicAndSize<T>(ref T data, uint magic) where T : unmanaged
{
uint actualMagic = 0;
int size = 0;
Read(ref actualMagic);
Read(ref size);
if (actualMagic != magic)
{
throw new DiskCacheLoadException(DiskCacheLoadResult.FileCorruptedInvalidMagic);
}
// Structs are expected to expand but not shrink between versions.
if (size > Unsafe.SizeOf<T>())
{
throw new DiskCacheLoadException(DiskCacheLoadResult.FileCorruptedInvalidLength);
}
Span<byte> buffer = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref data, 1)).Slice(0, size);
for (int offset = 0; offset < buffer.Length;)
{
offset += _activeStream.Read(buffer.Slice(offset));
}
}
/// <summary>
/// Writes data into the stream.
/// </summary>
/// <typeparam name="T">Type of the data</typeparam>
/// <param name="data">Data to be written</param>
public void Write<T>(ref T data) where T : unmanaged
{
Span<byte> buffer = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref data, 1));
_activeStream.Write(buffer);
}
/// <summary>
/// Writes data prefixed with a magic and size into the stream.
/// </summary>
/// <typeparam name="T">Type of the data</typeparam>
/// <param name="data">Data to write</param>
/// <param name="magic">Magic value to write</param>
public void WriteWithMagicAndSize<T>(ref T data, uint magic) where T : unmanaged
{
int size = Unsafe.SizeOf<T>();
Write(ref magic);
Write(ref size);
Span<byte> buffer = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref data, 1));
_activeStream.Write(buffer);
}
/// <summary>
/// Indicates that all data that will be read from the stream has been compressed.
/// </summary>
public void BeginCompression()
{
CompressionAlgorithm algorithm = CompressionAlgorithm.None;
Read(ref algorithm);
if (algorithm == CompressionAlgorithm.Deflate)
{
_activeStream = new DeflateStream(_stream, CompressionMode.Decompress, true);
}
}
/// <summary>
/// Indicates that all data that will be written into the stream should be compressed.
/// </summary>
/// <param name="algorithm">Compression algorithm that should be used</param>
public void BeginCompression(CompressionAlgorithm algorithm)
{
Write(ref algorithm);
if (algorithm == CompressionAlgorithm.Deflate)
{
_activeStream = new DeflateStream(_stream, CompressionLevel.SmallestSize, true);
}
}
/// <summary>
/// Indicates the end of a compressed chunck.
/// </summary>
/// <remarks>
/// Any data written after this will not be compressed unless <see cref="BeginCompression(CompressionAlgorithm)"/> is called again.
/// Any data read after this will be assumed to be uncompressed unless <see cref="BeginCompression"/> is called again.
/// </remarks>
public void EndCompression()
{
if (_activeStream != _stream)
{
_activeStream.Dispose();
_activeStream = _stream;
}
}
/// <summary>
/// Reads compressed data from the stream.
/// </summary>
/// <remarks>
/// <paramref name="data"/> must have the exact length of the uncompressed data,
/// otherwise decompression will fail.
/// </remarks>
/// <param name="stream">Stream to read from</param>
/// <param name="data">Buffer to write the uncompressed data into</param>
public static void ReadCompressed(Stream stream, Span<byte> data)
{
CompressionAlgorithm algorithm = (CompressionAlgorithm)stream.ReadByte();
switch (algorithm)
{
case CompressionAlgorithm.None:
stream.Read(data);
break;
case CompressionAlgorithm.Deflate:
stream = new DeflateStream(stream, CompressionMode.Decompress, true);
for (int offset = 0; offset < data.Length;)
{
offset += stream.Read(data.Slice(offset));
}
stream.Dispose();
break;
}
}
/// <summary>
/// Compresses and writes the compressed data into the stream.
/// </summary>
/// <param name="stream">Stream to write into</param>
/// <param name="data">Data to compress</param>
/// <param name="algorithm">Compression algorithm to be used</param>
public static void WriteCompressed(Stream stream, ReadOnlySpan<byte> data, CompressionAlgorithm algorithm)
{
stream.WriteByte((byte)algorithm);
switch (algorithm)
{
case CompressionAlgorithm.None:
stream.Write(data);
break;
case CompressionAlgorithm.Deflate:
stream = new DeflateStream(stream, CompressionLevel.SmallestSize, true);
stream.Write(data);
stream.Dispose();
break;
}
}
}
}

View file

@ -0,0 +1,18 @@
namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
{
/// <summary>
/// Algorithm used to compress the cache.
/// </summary>
enum CompressionAlgorithm : byte
{
/// <summary>
/// No compression, the data is stored as-is.
/// </summary>
None,
/// <summary>
/// Deflate compression (RFC 1951).
/// </summary>
Deflate
}
}

View file

@ -0,0 +1,57 @@
using Ryujinx.Common.Logging;
using System.IO;
namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
{
/// <summary>
/// Common disk cache utility methods.
/// </summary>
static class DiskCacheCommon
{
/// <summary>
/// Opens a file for read or write.
/// </summary>
/// <param name="basePath">Base path of the file (should not include the file name)</param>
/// <param name="fileName">Name of the file</param>
/// <param name="writable">Indicates if the file will be read or written</param>
/// <returns>File stream</returns>
public static FileStream OpenFile(string basePath, string fileName, bool writable)
{
string fullPath = Path.Combine(basePath, fileName);
FileMode mode;
FileAccess access;
if (writable)
{
mode = FileMode.OpenOrCreate;
access = FileAccess.ReadWrite;
}
else
{
mode = FileMode.Open;
access = FileAccess.Read;
}
try
{
return new FileStream(fullPath, mode, access, FileShare.Read);
}
catch (IOException ioException)
{
Logger.Error?.Print(LogClass.Gpu, $"Could not access file \"{fullPath}\". {ioException.Message}");
throw new DiskCacheLoadException(DiskCacheLoadResult.NoAccess);
}
}
/// <summary>
/// Gets the compression algorithm that should be used when writing the disk cache.
/// </summary>
/// <returns>Compression algorithm</returns>
public static CompressionAlgorithm GetCompressionAlgorithm()
{
return CompressionAlgorithm.Deflate;
}
}
}

View file

@ -0,0 +1,202 @@
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Gpu.Image;
using Ryujinx.Graphics.Shader;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
{
/// <summary>
/// Represents a GPU state and memory accessor.
/// </summary>
class DiskCacheGpuAccessor : GpuAccessorBase, IGpuAccessor
{
private readonly ReadOnlyMemory<byte> _data;
private readonly ReadOnlyMemory<byte> _cb1Data;
private readonly ShaderSpecializationState _oldSpecState;
private readonly ShaderSpecializationState _newSpecState;
private readonly int _stageIndex;
private ResourceCounts _resourceCounts;
/// <summary>
/// Creates a new instance of the cached GPU state accessor for shader translation.
/// </summary>
/// <param name="context">GPU context</param>
/// <param name="data">The data of the shader</param>
/// <param name="cb1Data">The constant buffer 1 data of the shader</param>
/// <param name="oldSpecState">Shader specialization state of the cached shader</param>
/// <param name="newSpecState">Shader specialization state of the recompiled shader</param>
/// <param name="stageIndex">Shader stage index</param>
public DiskCacheGpuAccessor(
GpuContext context,
ReadOnlyMemory<byte> data,
ReadOnlyMemory<byte> cb1Data,
ShaderSpecializationState oldSpecState,
ShaderSpecializationState newSpecState,
ResourceCounts counts,
int stageIndex) : base(context)
{
_data = data;
_cb1Data = cb1Data;
_oldSpecState = oldSpecState;
_newSpecState = newSpecState;
_stageIndex = stageIndex;
_resourceCounts = counts;
}
/// <inheritdoc/>
public uint ConstantBuffer1Read(int offset)
{
if (offset + sizeof(uint) > _cb1Data.Length)
{
throw new DiskCacheLoadException(DiskCacheLoadResult.InvalidCb1DataLength);
}
return MemoryMarshal.Cast<byte, uint>(_cb1Data.Span.Slice(offset))[0];
}
/// <inheritdoc/>
public void Log(string message)
{
Logger.Warning?.Print(LogClass.Gpu, $"Shader translator: {message}");
}
/// <inheritdoc/>
public ReadOnlySpan<ulong> GetCode(ulong address, int minimumSize)
{
return MemoryMarshal.Cast<byte, ulong>(_data.Span.Slice((int)address));
}
/// <inheritdoc/>
public int QueryBindingConstantBuffer(int index)
{
return _resourceCounts.UniformBuffersCount++;
}
/// <inheritdoc/>
public int QueryBindingStorageBuffer(int index)
{
return _resourceCounts.StorageBuffersCount++;
}
/// <inheritdoc/>
public int QueryBindingTexture(int index)
{
return _resourceCounts.TexturesCount++;
}
/// <inheritdoc/>
public int QueryBindingImage(int index)
{
return _resourceCounts.ImagesCount++;
}
/// <inheritdoc/>
public int QueryComputeLocalSizeX() => _oldSpecState.ComputeState.LocalSizeX;
/// <inheritdoc/>
public int QueryComputeLocalSizeY() => _oldSpecState.ComputeState.LocalSizeY;
/// <inheritdoc/>
public int QueryComputeLocalSizeZ() => _oldSpecState.ComputeState.LocalSizeZ;
/// <inheritdoc/>
public int QueryComputeLocalMemorySize() => _oldSpecState.ComputeState.LocalMemorySize;
/// <inheritdoc/>
public int QueryComputeSharedMemorySize() => _oldSpecState.ComputeState.SharedMemorySize;
/// <inheritdoc/>
public uint QueryConstantBufferUse()
{
_newSpecState.RecordConstantBufferUse(_stageIndex, _oldSpecState.ConstantBufferUse[_stageIndex]);
return _oldSpecState.ConstantBufferUse[_stageIndex];
}
/// <inheritdoc/>
public InputTopology QueryPrimitiveTopology()
{
_newSpecState.RecordPrimitiveTopology();
return ConvertToInputTopology(_oldSpecState.GraphicsState.Topology, _oldSpecState.GraphicsState.TessellationMode);
}
/// <inheritdoc/>
public bool QueryTessCw()
{
return _oldSpecState.GraphicsState.TessellationMode.UnpackCw();
}
/// <inheritdoc/>
public TessPatchType QueryTessPatchType()
{
return _oldSpecState.GraphicsState.TessellationMode.UnpackPatchType();
}
/// <inheritdoc/>
public TessSpacing QueryTessSpacing()
{
return _oldSpecState.GraphicsState.TessellationMode.UnpackSpacing();
}
/// <inheritdoc/>
public TextureFormat QueryTextureFormat(int handle, int cbufSlot)
{
_newSpecState.RecordTextureFormat(_stageIndex, handle, cbufSlot);
(uint format, bool formatSrgb) = _oldSpecState.GetFormat(_stageIndex, handle, cbufSlot);
return ConvertToTextureFormat(format, formatSrgb);
}
/// <inheritdoc/>
public SamplerType QuerySamplerType(int handle, int cbufSlot)
{
_newSpecState.RecordTextureSamplerType(_stageIndex, handle, cbufSlot);
return _oldSpecState.GetTextureTarget(_stageIndex, handle, cbufSlot).ConvertSamplerType();
}
/// <inheritdoc/>
public bool QueryTextureCoordNormalized(int handle, int cbufSlot)
{
_newSpecState.RecordTextureCoordNormalized(_stageIndex, handle, cbufSlot);
return _oldSpecState.GetCoordNormalized(_stageIndex, handle, cbufSlot);
}
/// <inheritdoc/>
public bool QueryTransformFeedbackEnabled()
{
return _oldSpecState.TransformFeedbackDescriptors != null;
}
/// <inheritdoc/>
public ReadOnlySpan<byte> QueryTransformFeedbackVaryingLocations(int bufferIndex)
{
return _oldSpecState.TransformFeedbackDescriptors[bufferIndex].AsSpan();
}
/// <inheritdoc/>
public int QueryTransformFeedbackStride(int bufferIndex)
{
return _oldSpecState.TransformFeedbackDescriptors[bufferIndex].Stride;
}
/// <inheritdoc/>
public bool QueryEarlyZForce()
{
_newSpecState.RecordEarlyZForce();
return _oldSpecState.GraphicsState.EarlyZForce;
}
/// <inheritdoc/>
public void RegisterTexture(int handle, int cbufSlot)
{
if (!_oldSpecState.TextureRegistered(_stageIndex, handle, cbufSlot))
{
throw new DiskCacheLoadException(DiskCacheLoadResult.MissingTextureDescriptor);
}
(uint format, bool formatSrgb) = _oldSpecState.GetFormat(_stageIndex, handle, cbufSlot);
TextureTarget target = _oldSpecState.GetTextureTarget(_stageIndex, handle, cbufSlot);
bool coordNormalized = _oldSpecState.GetCoordNormalized(_stageIndex, handle, cbufSlot);
_newSpecState.RegisterTexture(_stageIndex, handle, cbufSlot, format, formatSrgb, target, coordNormalized);
}
}
}

View file

@ -0,0 +1,459 @@
using Ryujinx.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
{
/// <summary>
/// On-disk shader cache storage for guest code.
/// </summary>
class DiskCacheGuestStorage
{
private const uint TocMagic = (byte)'T' | ((byte)'O' << 8) | ((byte)'C' << 16) | ((byte)'G' << 24);
private const ushort VersionMajor = 1;
private const ushort VersionMinor = 0;
private const uint VersionPacked = ((uint)VersionMajor << 16) | VersionMinor;
private const string TocFileName = "guest.toc";
private const string DataFileName = "guest.data";
private readonly string _basePath;
/// <summary>
/// TOC (Table of contents) file header.
/// </summary>
private struct TocHeader
{
/// <summary>
/// Magic value, for validation and identification purposes.
/// </summary>
public uint Magic;
/// <summary>
/// File format version.
/// </summary>
public uint Version;
/// <summary>
/// Header padding.
/// </summary>
public uint Padding;
/// <summary>
/// Number of modifications to the file, also the shaders count.
/// </summary>
public uint ModificationsCount;
/// <summary>
/// Reserved space, to be used in the future. Write as zero.
/// </summary>
public ulong Reserved;
/// <summary>
/// Reserved space, to be used in the future. Write as zero.
/// </summary>
public ulong Reserved2;
}
/// <summary>
/// TOC (Table of contents) file entry.
/// </summary>
private struct TocEntry
{
/// <summary>
/// Offset of the data on the data file.
/// </summary>
public uint Offset;
/// <summary>
/// Code size.
/// </summary>
public uint CodeSize;
/// <summary>
/// Constant buffer 1 data size.
/// </summary>
public uint Cb1DataSize;
/// <summary>
/// Hash of the code and constant buffer data.
/// </summary>
public uint Hash;
}
/// <summary>
/// TOC (Table of contents) memory cache entry.
/// </summary>
private struct TocMemoryEntry
{
/// <summary>
/// Offset of the data on the data file.
/// </summary>
public uint Offset;
/// <summary>
/// Code size.
/// </summary>
public uint CodeSize;
/// <summary>
/// Constant buffer 1 data size.
/// </summary>
public uint Cb1DataSize;
/// <summary>
/// Index of the shader on the cache.
/// </summary>
public readonly int Index;
/// <summary>
/// Creates a new TOC memory entry.
/// </summary>
/// <param name="offset">Offset of the data on the data file</param>
/// <param name="codeSize">Code size</param>
/// <param name="cb1DataSize">Constant buffer 1 data size</param>
/// <param name="index">Index of the shader on the cache</param>
public TocMemoryEntry(uint offset, uint codeSize, uint cb1DataSize, int index)
{
Offset = offset;
CodeSize = codeSize;
Cb1DataSize = cb1DataSize;
Index = index;
}
}
private Dictionary<uint, List<TocMemoryEntry>> _toc;
private uint _tocModificationsCount;
private (byte[], byte[])[] _cache;
/// <summary>
/// Creates a new disk cache guest storage.
/// </summary>
/// <param name="basePath">Base path of the disk shader cache</param>
public DiskCacheGuestStorage(string basePath)
{
_basePath = basePath;
}
/// <summary>
/// Checks if the TOC (table of contents) file for the guest cache exists.
/// </summary>
/// <returns>True if the file exists, false otherwise</returns>
public bool TocFileExists()
{
return File.Exists(Path.Combine(_basePath, TocFileName));
}
/// <summary>
/// Checks if the data file for the guest cache exists.
/// </summary>
/// <returns>True if the file exists, false otherwise</returns>
public bool DataFileExists()
{
return File.Exists(Path.Combine(_basePath, DataFileName));
}
/// <summary>
/// Opens the guest cache TOC (table of contents) file.
/// </summary>
/// <returns>File stream</returns>
public Stream OpenTocFileStream()
{
return DiskCacheCommon.OpenFile(_basePath, TocFileName, writable: false);
}
/// <summary>
/// Opens the guest cache data file.
/// </summary>
/// <returns>File stream</returns>
public Stream OpenDataFileStream()
{
return DiskCacheCommon.OpenFile(_basePath, DataFileName, writable: false);
}
/// <summary>
/// Clear all content from the guest cache files.
/// </summary>
public void ClearCache()
{
using var tocFileStream = DiskCacheCommon.OpenFile(_basePath, TocFileName, writable: true);
using var dataFileStream = DiskCacheCommon.OpenFile(_basePath, DataFileName, writable: true);
tocFileStream.SetLength(0);
dataFileStream.SetLength(0);
}
/// <summary>
/// Loads the guest cache from file or memory cache.
/// </summary>
/// <param name="tocFileStream">Guest TOC file stream</param>
/// <param name="dataFileStream">Guest data file stream</param>
/// <param name="index">Guest shader index</param>
/// <returns>Tuple with the guest code and constant buffer 1 data, respectively</returns>
public (byte[], byte[]) LoadShader(Stream tocFileStream, Stream dataFileStream, int index)
{
if (_cache == null || index >= _cache.Length)
{
_cache = new (byte[], byte[])[Math.Max(index + 1, GetShadersCountFromLength(tocFileStream.Length))];
}
(byte[] guestCode, byte[] cb1Data) = _cache[index];
if (guestCode == null || cb1Data == null)
{
BinarySerializer tocReader = new BinarySerializer(tocFileStream);
tocFileStream.Seek(Unsafe.SizeOf<TocHeader>() + index * Unsafe.SizeOf<TocEntry>(), SeekOrigin.Begin);
TocEntry entry = new TocEntry();
tocReader.Read(ref entry);
guestCode = new byte[entry.CodeSize];
cb1Data = new byte[entry.Cb1DataSize];
if (entry.Offset >= (ulong)dataFileStream.Length)
{
throw new DiskCacheLoadException(DiskCacheLoadResult.FileCorruptedGeneric);
}
dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin);
dataFileStream.Read(cb1Data);
BinarySerializer.ReadCompressed(dataFileStream, guestCode);
_cache[index] = (guestCode, cb1Data);
}
return (guestCode, cb1Data);
}
/// <summary>
/// Clears guest code memory cache, forcing future loads to be from file.
/// </summary>
public void ClearMemoryCache()
{
_cache = null;
}
/// <summary>
/// Calculates the guest shaders count from the TOC file length.
/// </summary>
/// <param name="length">TOC file length</param>
/// <returns>Shaders count</returns>
private static int GetShadersCountFromLength(long length)
{
return (int)((length - Unsafe.SizeOf<TocHeader>()) / Unsafe.SizeOf<TocEntry>());
}
/// <summary>
/// Adds a guest shader to the cache.
/// </summary>
/// <remarks>
/// If the shader is already on the cache, the existing index will be returned and nothing will be written.
/// </remarks>
/// <param name="data">Guest code</param>
/// <param name="cb1Data">Constant buffer 1 data accessed by the code</param>
/// <returns>Index of the shader on the cache</returns>
public int AddShader(ReadOnlySpan<byte> data, ReadOnlySpan<byte> cb1Data)
{
using var tocFileStream = DiskCacheCommon.OpenFile(_basePath, TocFileName, writable: true);
using var dataFileStream = DiskCacheCommon.OpenFile(_basePath, DataFileName, writable: true);
TocHeader header = new TocHeader();
LoadOrCreateToc(tocFileStream, ref header);
uint hash = CalcHash(data, cb1Data);
if (_toc.TryGetValue(hash, out var list))
{
foreach (var entry in list)
{
if (data.Length != entry.CodeSize || cb1Data.Length != entry.Cb1DataSize)
{
continue;
}
dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin);
byte[] cachedCode = new byte[entry.CodeSize];
byte[] cachedCb1Data = new byte[entry.Cb1DataSize];
dataFileStream.Read(cachedCb1Data);
BinarySerializer.ReadCompressed(dataFileStream, cachedCode);
if (data.SequenceEqual(cachedCode) && cb1Data.SequenceEqual(cachedCb1Data))
{
return entry.Index;
}
}
}
return WriteNewEntry(tocFileStream, dataFileStream, ref header, data, cb1Data, hash);
}
/// <summary>
/// Loads the guest cache TOC file, or create a new one if not present.
/// </summary>
/// <param name="tocFileStream">Guest TOC file stream</param>
/// <param name="header">Set to the TOC file header</param>
private void LoadOrCreateToc(Stream tocFileStream, ref TocHeader header)
{
BinarySerializer reader = new BinarySerializer(tocFileStream);
if (!reader.TryRead(ref header) || header.Magic != TocMagic || header.Version != VersionPacked)
{
CreateToc(tocFileStream, ref header);
}
if (_toc == null || header.ModificationsCount != _tocModificationsCount)
{
if (!LoadTocEntries(tocFileStream, ref reader))
{
CreateToc(tocFileStream, ref header);
}
_tocModificationsCount = header.ModificationsCount;
}
}
/// <summary>
/// Creates a new guest cache TOC file.
/// </summary>
/// <param name="tocFileStream">Guest TOC file stream</param>
/// <param name="header">Set to the TOC header</param>
private void CreateToc(Stream tocFileStream, ref TocHeader header)
{
BinarySerializer writer = new BinarySerializer(tocFileStream);
header.Magic = TocMagic;
header.Version = VersionPacked;
header.Padding = 0;
header.ModificationsCount = 0;
header.Reserved = 0;
header.Reserved2 = 0;
if (tocFileStream.Length > 0)
{
tocFileStream.Seek(0, SeekOrigin.Begin);
tocFileStream.SetLength(0);
}
writer.Write(ref header);
}
/// <summary>
/// Reads all the entries on the guest TOC file.
/// </summary>
/// <param name="tocFileStream">Guest TOC file stream</param>
/// <param name="reader">TOC file reader</param>
/// <returns>True if the operation was successful, false otherwise</returns>
private bool LoadTocEntries(Stream tocFileStream, ref BinarySerializer reader)
{
_toc = new Dictionary<uint, List<TocMemoryEntry>>();
TocEntry entry = new TocEntry();
int index = 0;
while (tocFileStream.Position < tocFileStream.Length)
{
if (!reader.TryRead(ref entry))
{
return false;
}
AddTocMemoryEntry(entry.Offset, entry.CodeSize, entry.Cb1DataSize, entry.Hash, index++);
}
return true;
}
/// <summary>
/// Writes a new guest code entry into the file.
/// </summary>
/// <param name="tocFileStream">TOC file stream</param>
/// <param name="dataFileStream">Data file stream</param>
/// <param name="header">TOC header, to be updated with the new count</param>
/// <param name="data">Guest code</param>
/// <param name="cb1Data">Constant buffer 1 data accessed by the guest code</param>
/// <param name="hash">Code and constant buffer data hash</param>
/// <returns>Entry index</returns>
private int WriteNewEntry(
Stream tocFileStream,
Stream dataFileStream,
ref TocHeader header,
ReadOnlySpan<byte> data,
ReadOnlySpan<byte> cb1Data,
uint hash)
{
BinarySerializer tocWriter = new BinarySerializer(tocFileStream);
dataFileStream.Seek(0, SeekOrigin.End);
uint dataOffset = checked((uint)dataFileStream.Position);
uint codeSize = (uint)data.Length;
uint cb1DataSize = (uint)cb1Data.Length;
dataFileStream.Write(cb1Data);
BinarySerializer.WriteCompressed(dataFileStream, data, DiskCacheCommon.GetCompressionAlgorithm());
_tocModificationsCount = ++header.ModificationsCount;
tocFileStream.Seek(0, SeekOrigin.Begin);
tocWriter.Write(ref header);
TocEntry entry = new TocEntry()
{
Offset = dataOffset,
CodeSize = codeSize,
Cb1DataSize = cb1DataSize,
Hash = hash
};
tocFileStream.Seek(0, SeekOrigin.End);
int index = (int)((tocFileStream.Position - Unsafe.SizeOf<TocHeader>()) / Unsafe.SizeOf<TocEntry>());
tocWriter.Write(ref entry);
AddTocMemoryEntry(dataOffset, codeSize, cb1DataSize, hash, index);
return index;
}
/// <summary>
/// Adds an entry to the memory TOC cache. This can be used to avoid reading the TOC file all the time.
/// </summary>
/// <param name="dataOffset">Offset of the code and constant buffer data in the data file</param>
/// <param name="codeSize">Code size</param>
/// <param name="cb1DataSize">Constant buffer 1 data size</param>
/// <param name="hash">Code and constant buffer data hash</param>
/// <param name="index">Index of the data on the cache</param>
private void AddTocMemoryEntry(uint dataOffset, uint codeSize, uint cb1DataSize, uint hash, int index)
{
if (!_toc.TryGetValue(hash, out var list))
{
_toc.Add(hash, list = new List<TocMemoryEntry>());
}
list.Add(new TocMemoryEntry(dataOffset, codeSize, cb1DataSize, index));
}
/// <summary>
/// Calculates the hash for a data pair.
/// </summary>
/// <param name="data">Data 1</param>
/// <param name="data2">Data 2</param>
/// <returns>Hash of both data</returns>
private static uint CalcHash(ReadOnlySpan<byte> data, ReadOnlySpan<byte> data2)
{
return CalcHash(data2) * 23 ^ CalcHash(data);
}
/// <summary>
/// Calculates the hash for data.
/// </summary>
/// <param name="data">Data to be hashed</param>
/// <returns>Hash of the data</returns>
private static uint CalcHash(ReadOnlySpan<byte> data)
{
return (uint)XXHash128.ComputeHash(data).Low;
}
}
}

View file

@ -0,0 +1,763 @@
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader;
using System;
using System.IO;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
{
/// <summary>
/// On-disk shader cache storage for host code.
/// </summary>
class DiskCacheHostStorage
{
private const uint TocsMagic = (byte)'T' | ((byte)'O' << 8) | ((byte)'C' << 16) | ((byte)'S' << 24);
private const uint TochMagic = (byte)'T' | ((byte)'O' << 8) | ((byte)'C' << 16) | ((byte)'H' << 24);
private const uint ShdiMagic = (byte)'S' | ((byte)'H' << 8) | ((byte)'D' << 16) | ((byte)'I' << 24);
private const uint BufdMagic = (byte)'B' | ((byte)'U' << 8) | ((byte)'F' << 16) | ((byte)'D' << 24);
private const uint TexdMagic = (byte)'T' | ((byte)'E' << 8) | ((byte)'X' << 16) | ((byte)'D' << 24);
private const ushort FileFormatVersionMajor = 1;
private const ushort FileFormatVersionMinor = 1;
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
private const uint CodeGenVersion = 0;
private const string SharedTocFileName = "shared.toc";
private const string SharedDataFileName = "shared.data";
private readonly string _basePath;
public bool CacheEnabled => !string.IsNullOrEmpty(_basePath);
/// <summary>
/// TOC (Table of contents) file header.
/// </summary>
private struct TocHeader
{
/// <summary>
/// Magic value, for validation and identification.
/// </summary>
public uint Magic;
/// <summary>
/// File format version.
/// </summary>
public uint FormatVersion;
/// <summary>
/// Generated shader code version.
/// </summary>
public uint CodeGenVersion;
/// <summary>
/// Header padding.
/// </summary>
public uint Padding;
/// <summary>
/// Reserved space, to be used in the future. Write as zero.
/// </summary>
public ulong Reserved;
/// <summary>
/// Reserved space, to be used in the future. Write as zero.
/// </summary>
public ulong Reserved2;
}
/// <summary>
/// Offset and size pair.
/// </summary>
private struct OffsetAndSize
{
/// <summary>
/// Offset.
/// </summary>
public ulong Offset;
/// <summary>
/// Size.
/// </summary>
public uint Size;
}
/// <summary>
/// Per-stage data entry.
/// </summary>
private struct DataEntryPerStage
{
/// <summary>
/// Index of the guest code on the guest code cache TOC file.
/// </summary>
public int GuestCodeIndex;
}
/// <summary>
/// Per-program data entry.
/// </summary>
private struct DataEntry
{
/// <summary>
/// Bit mask where each bit set is a used shader stage. Should be zero for compute shaders.
/// </summary>
public uint StagesBitMask;
}
/// <summary>
/// Per-stage shader information, returned by the translator.
/// </summary>
private struct DataShaderInfo
{
/// <summary>
/// Total constant buffers used.
/// </summary>
public ushort CBuffersCount;
/// <summary>
/// Total storage buffers used.
/// </summary>
public ushort SBuffersCount;
/// <summary>
/// Total textures used.
/// </summary>
public ushort TexturesCount;
/// <summary>
/// Total images used.
/// </summary>
public ushort ImagesCount;
/// <summary>
/// Shader stage.
/// </summary>
public ShaderStage Stage;
/// <summary>
/// Indicates if the shader accesses the Instance ID built-in variable.
/// </summary>
public bool UsesInstanceId;
/// <summary>
/// Indicates if the shader modifies the Layer built-in variable.
/// </summary>
public bool UsesRtLayer;
/// <summary>
/// Bit mask with the clip distances written on the vertex stage.
/// </summary>
public byte ClipDistancesWritten;
/// <summary>
/// Bit mask of the render target components written by the fragment stage.
/// </summary>
public int FragmentOutputMap;
}
private readonly DiskCacheGuestStorage _guestStorage;
/// <summary>
/// Creates a disk cache host storage.
/// </summary>
/// <param name="basePath">Base path of the shader cache</param>
public DiskCacheHostStorage(string basePath)
{
_basePath = basePath;
_guestStorage = new DiskCacheGuestStorage(basePath);
if (CacheEnabled)
{
Directory.CreateDirectory(basePath);
}
}
/// <summary>
/// Gets the total of host programs on the cache.
/// </summary>
/// <returns>Host programs count</returns>
public int GetProgramCount()
{
string tocFilePath = Path.Combine(_basePath, SharedTocFileName);
if (!File.Exists(tocFilePath))
{
return 0;
}
return (int)((new FileInfo(tocFilePath).Length - Unsafe.SizeOf<TocHeader>()) / sizeof(ulong));
}
/// <summary>
/// Guest the name of the host program cache file, with extension.
/// </summary>
/// <param name="context">GPU context</param>
/// <returns>Name of the file, without extension</returns>
private static string GetHostFileName(GpuContext context)
{
string apiName = context.Capabilities.Api.ToString().ToLowerInvariant();
string vendorName = RemoveInvalidCharacters(context.Capabilities.VendorName.ToLowerInvariant());
return $"{apiName}_{vendorName}";
}
/// <summary>
/// Removes invalid path characters and spaces from a file name.
/// </summary>
/// <param name="fileName">File name</param>
/// <returns>Filtered file name</returns>
private static string RemoveInvalidCharacters(string fileName)
{
int indexOfSpace = fileName.IndexOf(' ');
if (indexOfSpace >= 0)
{
fileName = fileName.Substring(0, indexOfSpace);
}
return string.Concat(fileName.Split(Path.GetInvalidFileNameChars(), StringSplitOptions.RemoveEmptyEntries));
}
/// <summary>
/// Gets the name of the TOC host file.
/// </summary>
/// <param name="context">GPU context</param>
/// <returns>File name</returns>
private static string GetHostTocFileName(GpuContext context)
{
return GetHostFileName(context) + ".toc";
}
/// <summary>
/// Gets the name of the data host file.
/// </summary>
/// <param name="context">GPU context</param>
/// <returns>File name</returns>
private static string GetHostDataFileName(GpuContext context)
{
return GetHostFileName(context) + ".data";
}
/// <summary>
/// Checks if a disk cache exists for the current application.
/// </summary>
/// <returns>True if a disk cache exists, false otherwise</returns>
public bool CacheExists()
{
string tocFilePath = Path.Combine(_basePath, SharedTocFileName);
string dataFilePath = Path.Combine(_basePath, SharedDataFileName);
if (!File.Exists(tocFilePath) || !File.Exists(dataFilePath) || !_guestStorage.TocFileExists() || !_guestStorage.DataFileExists())
{
return false;
}
return true;
}
/// <summary>
/// Loads all shaders from the cache.
/// </summary>
/// <param name="context">GPU context</param>
/// <param name="loader">Parallel disk cache loader</param>
public void LoadShaders(GpuContext context, ParallelDiskCacheLoader loader)
{
if (!CacheExists())
{
return;
}
Stream hostTocFileStream = null;
Stream hostDataFileStream = null;
try
{
using var tocFileStream = DiskCacheCommon.OpenFile(_basePath, SharedTocFileName, writable: false);
using var dataFileStream = DiskCacheCommon.OpenFile(_basePath, SharedDataFileName, writable: false);
using var guestTocFileStream = _guestStorage.OpenTocFileStream();
using var guestDataFileStream = _guestStorage.OpenDataFileStream();
BinarySerializer tocReader = new BinarySerializer(tocFileStream);
BinarySerializer dataReader = new BinarySerializer(dataFileStream);
TocHeader header = new TocHeader();
if (!tocReader.TryRead(ref header) || header.Magic != TocsMagic)
{
throw new DiskCacheLoadException(DiskCacheLoadResult.FileCorruptedGeneric);
}
if (header.FormatVersion != FileFormatVersionPacked)
{
throw new DiskCacheLoadException(DiskCacheLoadResult.IncompatibleVersion);
}
bool loadHostCache = header.CodeGenVersion == CodeGenVersion;
int programIndex = 0;
DataEntry entry = new DataEntry();
while (tocFileStream.Position < tocFileStream.Length && loader.Active)
{
ulong dataOffset = 0;
tocReader.Read(ref dataOffset);
if ((ulong)dataOffset >= (ulong)dataFileStream.Length)
{
throw new DiskCacheLoadException(DiskCacheLoadResult.FileCorruptedGeneric);
}
dataFileStream.Seek((long)dataOffset, SeekOrigin.Begin);
dataReader.BeginCompression();
dataReader.Read(ref entry);
uint stagesBitMask = entry.StagesBitMask;
if ((stagesBitMask & ~0x3fu) != 0)
{
throw new DiskCacheLoadException(DiskCacheLoadResult.FileCorruptedGeneric);
}
bool isCompute = stagesBitMask == 0;
if (isCompute)
{
stagesBitMask = 1;
}
CachedShaderStage[] shaders = new CachedShaderStage[isCompute ? 1 : Constants.ShaderStages + 1];
DataEntryPerStage stageEntry = new DataEntryPerStage();
while (stagesBitMask != 0)
{
int stageIndex = BitOperations.TrailingZeroCount(stagesBitMask);
dataReader.Read(ref stageEntry);
ShaderProgramInfo info = stageIndex != 0 || isCompute ? ReadShaderProgramInfo(ref dataReader) : null;
(byte[] guestCode, byte[] cb1Data) = _guestStorage.LoadShader(
guestTocFileStream,
guestDataFileStream,
stageEntry.GuestCodeIndex);
shaders[stageIndex] = new CachedShaderStage(info, guestCode, cb1Data);
stagesBitMask &= ~(1u << stageIndex);
}
ShaderSpecializationState specState = ShaderSpecializationState.Read(ref dataReader);
dataReader.EndCompression();
if (loadHostCache)
{
byte[] hostCode = ReadHostCode(context, ref hostTocFileStream, ref hostDataFileStream, programIndex);
if (hostCode != null)
{
bool hasFragmentShader = shaders.Length > 5 && shaders[5] != null;
int fragmentOutputMap = hasFragmentShader ? shaders[5].Info.FragmentOutputMap : -1;
IProgram hostProgram = context.Renderer.LoadProgramBinary(hostCode, hasFragmentShader, new ShaderInfo(fragmentOutputMap));
CachedShaderProgram program = new CachedShaderProgram(hostProgram, specState, shaders);
loader.QueueHostProgram(program, hostProgram, programIndex, isCompute);
}
else
{
loadHostCache = false;
}
}
if (!loadHostCache)
{
loader.QueueGuestProgram(shaders, specState, programIndex, isCompute);
}
loader.CheckCompilation();
programIndex++;
}
}
finally
{
_guestStorage.ClearMemoryCache();
hostTocFileStream?.Dispose();
hostDataFileStream?.Dispose();
}
}
/// <summary>
/// Reads the host code for a given shader, if existent.
/// </summary>
/// <param name="context">GPU context</param>
/// <param name="tocFileStream">Host TOC file stream, intialized if needed</param>
/// <param name="dataFileStream">Host data file stream, initialized if needed</param>
/// <param name="programIndex">Index of the program on the cache</param>
/// <returns>Host binary code, or null if not found</returns>
private byte[] ReadHostCode(GpuContext context, ref Stream tocFileStream, ref Stream dataFileStream, int programIndex)
{
if (tocFileStream == null && dataFileStream == null)
{
string tocFilePath = Path.Combine(_basePath, GetHostTocFileName(context));
string dataFilePath = Path.Combine(_basePath, GetHostDataFileName(context));
if (!File.Exists(tocFilePath) || !File.Exists(dataFilePath))
{
return null;
}
tocFileStream = DiskCacheCommon.OpenFile(_basePath, GetHostTocFileName(context), writable: false);
dataFileStream = DiskCacheCommon.OpenFile(_basePath, GetHostDataFileName(context), writable: false);
}
int offset = Unsafe.SizeOf<TocHeader>() + programIndex * Unsafe.SizeOf<OffsetAndSize>();
if (offset + Unsafe.SizeOf<OffsetAndSize>() > tocFileStream.Length)
{
return null;
}
if ((ulong)offset >= (ulong)dataFileStream.Length)
{
throw new DiskCacheLoadException(DiskCacheLoadResult.FileCorruptedGeneric);
}
tocFileStream.Seek(offset, SeekOrigin.Begin);
BinarySerializer tocReader = new BinarySerializer(tocFileStream);
OffsetAndSize offsetAndSize = new OffsetAndSize();
tocReader.Read(ref offsetAndSize);
if (offsetAndSize.Offset >= (ulong)dataFileStream.Length)
{
throw new DiskCacheLoadException(DiskCacheLoadResult.FileCorruptedGeneric);
}
dataFileStream.Seek((long)offsetAndSize.Offset, SeekOrigin.Begin);
byte[] hostCode = new byte[offsetAndSize.Size];
BinarySerializer.ReadCompressed(dataFileStream, hostCode);
return hostCode;
}
/// <summary>
/// Gets output streams for the disk cache, for faster batch writing.
/// </summary>
/// <param name="context">The GPU context, used to determine the host disk cache</param>
/// <returns>A collection of disk cache output streams</returns>
public DiskCacheOutputStreams GetOutputStreams(GpuContext context)
{
var tocFileStream = DiskCacheCommon.OpenFile(_basePath, SharedTocFileName, writable: true);
var dataFileStream = DiskCacheCommon.OpenFile(_basePath, SharedDataFileName, writable: true);
var hostTocFileStream = DiskCacheCommon.OpenFile(_basePath, GetHostTocFileName(context), writable: true);
var hostDataFileStream = DiskCacheCommon.OpenFile(_basePath, GetHostDataFileName(context), writable: true);
return new DiskCacheOutputStreams(tocFileStream, dataFileStream, hostTocFileStream, hostDataFileStream);
}
/// <summary>
/// Adds a shader to the cache.
/// </summary>
/// <param name="context">GPU context</param>
/// <param name="program">Cached program</param>
/// <param name="hostCode">Optional host binary code</param>
/// <param name="streams">Output streams to use</param>
public void AddShader(GpuContext context, CachedShaderProgram program, ReadOnlySpan<byte> hostCode, DiskCacheOutputStreams streams = null)
{
uint stagesBitMask = 0;
for (int index = 0; index < program.Shaders.Length; index++)
{
var shader = program.Shaders[index];
if (shader == null || (shader.Info != null && shader.Info.Stage == ShaderStage.Compute))
{
continue;
}
stagesBitMask |= 1u << index;
}
var tocFileStream = streams != null ? streams.TocFileStream : DiskCacheCommon.OpenFile(_basePath, SharedTocFileName, writable: true);
var dataFileStream = streams != null ? streams.DataFileStream : DiskCacheCommon.OpenFile(_basePath, SharedDataFileName, writable: true);
if (tocFileStream.Length == 0)
{
TocHeader header = new TocHeader();
CreateToc(tocFileStream, ref header, TocsMagic, CodeGenVersion);
}
tocFileStream.Seek(0, SeekOrigin.End);
dataFileStream.Seek(0, SeekOrigin.End);
BinarySerializer tocWriter = new BinarySerializer(tocFileStream);
BinarySerializer dataWriter = new BinarySerializer(dataFileStream);
ulong dataOffset = (ulong)dataFileStream.Position;
tocWriter.Write(ref dataOffset);
DataEntry entry = new DataEntry();
entry.StagesBitMask = stagesBitMask;
dataWriter.BeginCompression(DiskCacheCommon.GetCompressionAlgorithm());
dataWriter.Write(ref entry);
DataEntryPerStage stageEntry = new DataEntryPerStage();
for (int index = 0; index < program.Shaders.Length; index++)
{
var shader = program.Shaders[index];
if (shader == null)
{
continue;
}
stageEntry.GuestCodeIndex = _guestStorage.AddShader(shader.Code, shader.Cb1Data);
dataWriter.Write(ref stageEntry);
WriteShaderProgramInfo(ref dataWriter, shader.Info);
}
program.SpecializationState.Write(ref dataWriter);
dataWriter.EndCompression();
if (streams == null)
{
tocFileStream.Dispose();
dataFileStream.Dispose();
}
if (hostCode.IsEmpty)
{
return;
}
WriteHostCode(context, hostCode, -1, streams);
}
/// <summary>
/// Clears all content from the guest cache files.
/// </summary>
public void ClearGuestCache()
{
_guestStorage.ClearCache();
}
/// <summary>
/// Clears all content from the shared cache files.
/// </summary>
/// <param name="context">GPU context</param>
public void ClearSharedCache()
{
using var tocFileStream = DiskCacheCommon.OpenFile(_basePath, SharedTocFileName, writable: true);
using var dataFileStream = DiskCacheCommon.OpenFile(_basePath, SharedDataFileName, writable: true);
tocFileStream.SetLength(0);
dataFileStream.SetLength(0);
}
/// <summary>
/// Deletes all content from the host cache files.
/// </summary>
/// <param name="context">GPU context</param>
public void ClearHostCache(GpuContext context)
{
using var tocFileStream = DiskCacheCommon.OpenFile(_basePath, GetHostTocFileName(context), writable: true);
using var dataFileStream = DiskCacheCommon.OpenFile(_basePath, GetHostDataFileName(context), writable: true);
tocFileStream.SetLength(0);
dataFileStream.SetLength(0);
}
/// <summary>
/// Adds a host binary shader to the host cache.
/// </summary>
/// <remarks>
/// This only modifies the host cache. The shader must already exist in the other caches.
/// This method should only be used for rebuilding the host cache after a clear.
/// </remarks>
/// <param name="context">GPU context</param>
/// <param name="hostCode">Host binary code</param>
/// <param name="programIndex">Index of the program in the cache</param>
public void AddHostShader(GpuContext context, ReadOnlySpan<byte> hostCode, int programIndex)
{
WriteHostCode(context, hostCode, programIndex);
}
/// <summary>
/// Writes the host binary code on the host cache.
/// </summary>
/// <param name="context">GPU context</param>
/// <param name="hostCode">Host binary code</param>
/// <param name="programIndex">Index of the program in the cache</param>
/// <param name="streams">Output streams to use</param>
private void WriteHostCode(GpuContext context, ReadOnlySpan<byte> hostCode, int programIndex, DiskCacheOutputStreams streams = null)
{
var tocFileStream = streams != null ? streams.HostTocFileStream : DiskCacheCommon.OpenFile(_basePath, GetHostTocFileName(context), writable: true);
var dataFileStream = streams != null ? streams.HostDataFileStream : DiskCacheCommon.OpenFile(_basePath, GetHostDataFileName(context), writable: true);
if (tocFileStream.Length == 0)
{
TocHeader header = new TocHeader();
CreateToc(tocFileStream, ref header, TochMagic, 0);
}
if (programIndex == -1)
{
tocFileStream.Seek(0, SeekOrigin.End);
}
else
{
tocFileStream.Seek(Unsafe.SizeOf<TocHeader>() + (programIndex * Unsafe.SizeOf<OffsetAndSize>()), SeekOrigin.Begin);
}
dataFileStream.Seek(0, SeekOrigin.End);
BinarySerializer tocWriter = new BinarySerializer(tocFileStream);
OffsetAndSize offsetAndSize = new OffsetAndSize();
offsetAndSize.Offset = (ulong)dataFileStream.Position;
offsetAndSize.Size = (uint)hostCode.Length;
tocWriter.Write(ref offsetAndSize);
BinarySerializer.WriteCompressed(dataFileStream, hostCode, DiskCacheCommon.GetCompressionAlgorithm());
if (streams == null)
{
tocFileStream.Dispose();
dataFileStream.Dispose();
}
}
/// <summary>
/// Creates a TOC file for the host or shared cache.
/// </summary>
/// <param name="tocFileStream">TOC file stream</param>
/// <param name="header">Set to the TOC file header</param>
/// <param name="magic">Magic value to be written</param>
/// <param name="codegenVersion">Shader codegen version, only valid for the host file</param>
private void CreateToc(Stream tocFileStream, ref TocHeader header, uint magic, uint codegenVersion)
{
BinarySerializer writer = new BinarySerializer(tocFileStream);
header.Magic = magic;
header.FormatVersion = FileFormatVersionPacked;
header.CodeGenVersion = codegenVersion;
header.Padding = 0;
header.Reserved = 0;
header.Reserved2 = 0;
if (tocFileStream.Length > 0)
{
tocFileStream.Seek(0, SeekOrigin.Begin);
tocFileStream.SetLength(0);
}
writer.Write(ref header);
}
/// <summary>
/// Reads the shader program info from the cache.
/// </summary>
/// <param name="dataReader">Cache data reader</param>
/// <returns>Shader program info</returns>
private static ShaderProgramInfo ReadShaderProgramInfo(ref BinarySerializer dataReader)
{
DataShaderInfo dataInfo = new DataShaderInfo();
dataReader.ReadWithMagicAndSize(ref dataInfo, ShdiMagic);
BufferDescriptor[] cBuffers = new BufferDescriptor[dataInfo.CBuffersCount];
BufferDescriptor[] sBuffers = new BufferDescriptor[dataInfo.SBuffersCount];
TextureDescriptor[] textures = new TextureDescriptor[dataInfo.TexturesCount];
TextureDescriptor[] images = new TextureDescriptor[dataInfo.ImagesCount];
for (int index = 0; index < dataInfo.CBuffersCount; index++)
{
dataReader.ReadWithMagicAndSize(ref cBuffers[index], BufdMagic);
}
for (int index = 0; index < dataInfo.SBuffersCount; index++)
{
dataReader.ReadWithMagicAndSize(ref sBuffers[index], BufdMagic);
}
for (int index = 0; index < dataInfo.TexturesCount; index++)
{
dataReader.ReadWithMagicAndSize(ref textures[index], TexdMagic);
}
for (int index = 0; index < dataInfo.ImagesCount; index++)
{
dataReader.ReadWithMagicAndSize(ref images[index], TexdMagic);
}
return new ShaderProgramInfo(
cBuffers,
sBuffers,
textures,
images,
dataInfo.Stage,
dataInfo.UsesInstanceId,
dataInfo.UsesRtLayer,
dataInfo.ClipDistancesWritten,
dataInfo.FragmentOutputMap);
}
/// <summary>
/// Writes the shader program info into the cache.
/// </summary>
/// <param name="dataWriter">Cache data writer</param>
/// <param name="info">Program info</param>
private static void WriteShaderProgramInfo(ref BinarySerializer dataWriter, ShaderProgramInfo info)
{
if (info == null)
{
return;
}
DataShaderInfo dataInfo = new DataShaderInfo();
dataInfo.CBuffersCount = (ushort)info.CBuffers.Count;
dataInfo.SBuffersCount = (ushort)info.SBuffers.Count;
dataInfo.TexturesCount = (ushort)info.Textures.Count;
dataInfo.ImagesCount = (ushort)info.Images.Count;
dataInfo.Stage = info.Stage;
dataInfo.UsesInstanceId = info.UsesInstanceId;
dataInfo.UsesRtLayer = info.UsesRtLayer;
dataInfo.ClipDistancesWritten = info.ClipDistancesWritten;
dataInfo.FragmentOutputMap = info.FragmentOutputMap;
dataWriter.WriteWithMagicAndSize(ref dataInfo, ShdiMagic);
for (int index = 0; index < info.CBuffers.Count; index++)
{
var entry = info.CBuffers[index];
dataWriter.WriteWithMagicAndSize(ref entry, BufdMagic);
}
for (int index = 0; index < info.SBuffers.Count; index++)
{
var entry = info.SBuffers[index];
dataWriter.WriteWithMagicAndSize(ref entry, BufdMagic);
}
for (int index = 0; index < info.Textures.Count; index++)
{
var entry = info.Textures[index];
dataWriter.WriteWithMagicAndSize(ref entry, TexdMagic);
}
for (int index = 0; index < info.Images.Count; index++)
{
var entry = info.Images[index];
dataWriter.WriteWithMagicAndSize(ref entry, TexdMagic);
}
}
}
}

View file

@ -0,0 +1,48 @@
using System;
namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
{
/// <summary>
/// Disk cache load exception.
/// </summary>
class DiskCacheLoadException : Exception
{
/// <summary>
/// Result of the cache load operation.
/// </summary>
public DiskCacheLoadResult Result { get; }
/// <summary>
/// Creates a new instance of the disk cache load exception.
/// </summary>
public DiskCacheLoadException()
{
}
/// <summary>
/// Creates a new instance of the disk cache load exception.
/// </summary>
/// <param name="message">Exception message</param>
public DiskCacheLoadException(string message) : base(message)
{
}
/// <summary>
/// Creates a new instance of the disk cache load exception.
/// </summary>
/// <param name="message">Exception message</param>
/// <param name="inner">Inner exception</param>
public DiskCacheLoadException(string message, Exception inner) : base(message, inner)
{
}
/// <summary>
/// Creates a new instance of the disk cache load exception.
/// </summary>
/// <param name="result">Result code</param>
public DiskCacheLoadException(DiskCacheLoadResult result) : base(result.GetMessage())
{
Result = result;
}
}
}

View file

@ -0,0 +1,72 @@
namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
{
/// <summary>
/// Result of a shader cache load operation.
/// </summary>
enum DiskCacheLoadResult
{
/// <summary>
/// No error.
/// </summary>
Success,
/// <summary>
/// File can't be accessed.
/// </summary>
NoAccess,
/// <summary>
/// The constant buffer 1 data length is too low for the translation of the guest shader.
/// </summary>
InvalidCb1DataLength,
/// <summary>
/// The cache is missing the descriptor of a texture used by the shader.
/// </summary>
MissingTextureDescriptor,
/// <summary>
/// File is corrupted.
/// </summary>
FileCorruptedGeneric,
/// <summary>
/// File is corrupted, detected by magic value check.
/// </summary>
FileCorruptedInvalidMagic,
/// <summary>
/// File is corrupted, detected by length check.
/// </summary>
FileCorruptedInvalidLength,
/// <summary>
/// File might be valid, but is incompatible with the current emulator version.
/// </summary>
IncompatibleVersion
}
static class DiskCacheLoadResultExtensions
{
/// <summary>
/// Gets an error message from a result code.
/// </summary>
/// <param name="result">Result code</param>
/// <returns>Error message</returns>
public static string GetMessage(this DiskCacheLoadResult result)
{
return result switch
{
DiskCacheLoadResult.Success => "No error.",
DiskCacheLoadResult.NoAccess => "Could not access the cache file.",
DiskCacheLoadResult.InvalidCb1DataLength => "Constant buffer 1 data length is too low.",
DiskCacheLoadResult.MissingTextureDescriptor => "Texture descriptor missing from the cache file.",
DiskCacheLoadResult.FileCorruptedGeneric => "The cache file is corrupted.",
DiskCacheLoadResult.FileCorruptedInvalidMagic => "Magic check failed, the cache file is corrupted.",
DiskCacheLoadResult.FileCorruptedInvalidLength => "Length check failed, the cache file is corrupted.",
DiskCacheLoadResult.IncompatibleVersion => "The version of the disk cache is not compatible with this version of the emulator.",
_ => "Unknown error."
};
}
}
}

View file

@ -0,0 +1,57 @@
using System;
using System.IO;
namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
{
/// <summary>
/// Output streams for the disk shader cache.
/// </summary>
class DiskCacheOutputStreams : IDisposable
{
/// <summary>
/// Shared table of contents (TOC) file stream.
/// </summary>
public readonly FileStream TocFileStream;
/// <summary>
/// Shared data file stream.
/// </summary>
public readonly FileStream DataFileStream;
/// <summary>
/// Host table of contents (TOC) file stream.
/// </summary>
public readonly FileStream HostTocFileStream;
/// <summary>
/// Host data file stream.
/// </summary>
public readonly FileStream HostDataFileStream;
/// <summary>
/// Creates a new instance of a disk cache output stream container.
/// </summary>
/// <param name="tocFileStream">Stream for the shared table of contents file</param>
/// <param name="dataFileStream">Stream for the shared data file</param>
/// <param name="hostTocFileStream">Stream for the host table of contents file</param>
/// <param name="hostDataFileStream">Stream for the host data file</param>
public DiskCacheOutputStreams(FileStream tocFileStream, FileStream dataFileStream, FileStream hostTocFileStream, FileStream hostDataFileStream)
{
TocFileStream = tocFileStream;
DataFileStream = dataFileStream;
HostTocFileStream = hostTocFileStream;
HostDataFileStream = hostDataFileStream;
}
/// <summary>
/// Disposes the output file streams.
/// </summary>
public void Dispose()
{
TocFileStream.Dispose();
DataFileStream.Dispose();
HostTocFileStream.Dispose();
HostDataFileStream.Dispose();
}
}
}

View file

@ -0,0 +1,672 @@
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader;
using Ryujinx.Graphics.Shader.Translation;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using static Ryujinx.Graphics.Gpu.Shader.ShaderCache;
namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
{
class ParallelDiskCacheLoader
{
private const int ThreadCount = 8;
private readonly GpuContext _context;
private readonly ShaderCacheHashTable _graphicsCache;
private readonly ComputeShaderCacheHashTable _computeCache;
private readonly DiskCacheHostStorage _hostStorage;
private readonly CancellationToken _cancellationToken;
private readonly Action<ShaderCacheState, int, int> _stateChangeCallback;
/// <summary>
/// Indicates if the cache should be loaded.
/// </summary>
public bool Active => !_cancellationToken.IsCancellationRequested;
private bool _needsHostRegen;
/// <summary>
/// Number of shaders that failed to compile from the cache.
/// </summary>
public int ErrorCount { get; private set; }
/// <summary>
/// Program validation entry.
/// </summary>
private struct ProgramEntry
{
/// <summary>
/// Cached shader program.
/// </summary>
public readonly CachedShaderProgram CachedProgram;
/// <summary>
/// Host program.
/// </summary>
public readonly IProgram HostProgram;
/// <summary>
/// Program index.
/// </summary>
public readonly int ProgramIndex;
/// <summary>
/// Indicates if the program is a compute shader.
/// </summary>
public readonly bool IsCompute;
/// <summary>
/// Indicates if the program is a host binary shader.
/// </summary>
public readonly bool IsBinary;
/// <summary>
/// Creates a new program validation entry.
/// </summary>
/// <param name="cachedProgram">Cached shader program</param>
/// <param name="hostProgram">Host program</param>
/// <param name="programIndex">Program index</param>
/// <param name="isCompute">Indicates if the program is a compute shader</param>
/// <param name="isBinary">Indicates if the program is a host binary shader</param>
public ProgramEntry(
CachedShaderProgram cachedProgram,
IProgram hostProgram,
int programIndex,
bool isCompute,
bool isBinary)
{
CachedProgram = cachedProgram;
HostProgram = hostProgram;
ProgramIndex = programIndex;
IsCompute = isCompute;
IsBinary = isBinary;
}
}
/// <summary>
/// Translated shader compilation entry.
/// </summary>
private struct ProgramCompilation
{
/// <summary>
/// Translated shader stages.
/// </summary>
public readonly ShaderProgram[] TranslatedStages;
/// <summary>
/// Cached shaders.
/// </summary>
public readonly CachedShaderStage[] Shaders;
/// <summary>
/// Specialization state.
/// </summary>
public readonly ShaderSpecializationState SpecializationState;
/// <summary>
/// Program index.
/// </summary>
public readonly int ProgramIndex;
/// <summary>
/// Indicates if the program is a compute shader.
/// </summary>
public readonly bool IsCompute;
/// <summary>
/// Creates a new translated shader compilation entry.
/// </summary>
/// <param name="translatedStages">Translated shader stages</param>
/// <param name="shaders">Cached shaders</param>
/// <param name="specState">Specialization state</param>
/// <param name="programIndex">Program index</param>
/// <param name="isCompute">Indicates if the program is a compute shader</param>
public ProgramCompilation(
ShaderProgram[] translatedStages,
CachedShaderStage[] shaders,
ShaderSpecializationState specState,
int programIndex,
bool isCompute)
{
TranslatedStages = translatedStages;
Shaders = shaders;
SpecializationState = specState;
ProgramIndex = programIndex;
IsCompute = isCompute;
}
}
/// <summary>
/// Program translation entry.
/// </summary>
private struct AsyncProgramTranslation
{
/// <summary>
/// Cached shader stages.
/// </summary>
public readonly CachedShaderStage[] Shaders;
/// <summary>
/// Specialization state.
/// </summary>
public readonly ShaderSpecializationState SpecializationState;
/// <summary>
/// Program index.
/// </summary>
public readonly int ProgramIndex;
/// <summary>
/// Indicates if the program is a compute shader.
/// </summary>
public readonly bool IsCompute;
/// <summary>
/// Creates a new program translation entry.
/// </summary>
/// <param name="shaders">Cached shader stages</param>
/// <param name="specState">Specialization state</param>
/// <param name="programIndex">Program index</param>
/// <param name="isCompute">Indicates if the program is a compute shader</param>
public AsyncProgramTranslation(
CachedShaderStage[] shaders,
ShaderSpecializationState specState,
int programIndex,
bool isCompute)
{
Shaders = shaders;
SpecializationState = specState;
ProgramIndex = programIndex;
IsCompute = isCompute;
}
}
private readonly Queue<ProgramEntry> _validationQueue;
private readonly ConcurrentQueue<ProgramCompilation> _compilationQueue;
private readonly BlockingCollection<AsyncProgramTranslation> _asyncTranslationQueue;
private readonly SortedList<int, CachedShaderProgram> _programList;
private int _backendParallelCompileThreads;
private int _compiledCount;
private int _totalCount;
/// <summary>
/// Creates a new parallel disk cache loader.
/// </summary>
/// <param name="context">GPU context</param>
/// <param name="graphicsCache">Graphics shader cache</param>
/// <param name="computeCache">Compute shader cache</param>
/// <param name="hostStorage">Disk cache host storage</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="stateChangeCallback">Function to be called when there is a state change, reporting state, compiled and total shaders count</param>
public ParallelDiskCacheLoader(
GpuContext context,
ShaderCacheHashTable graphicsCache,
ComputeShaderCacheHashTable computeCache,
DiskCacheHostStorage hostStorage,
CancellationToken cancellationToken,
Action<ShaderCacheState, int, int> stateChangeCallback)
{
_context = context;
_graphicsCache = graphicsCache;
_computeCache = computeCache;
_hostStorage = hostStorage;
_cancellationToken = cancellationToken;
_stateChangeCallback = stateChangeCallback;
_validationQueue = new Queue<ProgramEntry>();
_compilationQueue = new ConcurrentQueue<ProgramCompilation>();
_asyncTranslationQueue = new BlockingCollection<AsyncProgramTranslation>(ThreadCount);
_programList = new SortedList<int, CachedShaderProgram>();
_backendParallelCompileThreads = Math.Min(Environment.ProcessorCount, 8); // Must be kept in sync with the backend code.
}
/// <summary>
/// Loads all shaders from the cache.
/// </summary>
public void LoadShaders()
{
Thread[] workThreads = new Thread[ThreadCount];
for (int index = 0; index < ThreadCount; index++)
{
workThreads[index] = new Thread(ProcessAsyncQueue)
{
Name = $"Gpu.AsyncTranslationThread.{index}"
};
}
int programCount = _hostStorage.GetProgramCount();
_compiledCount = 0;
_totalCount = programCount;
_stateChangeCallback(ShaderCacheState.Start, 0, programCount);
Logger.Info?.Print(LogClass.Gpu, $"Loading {programCount} shaders from the cache...");
for (int index = 0; index < ThreadCount; index++)
{
workThreads[index].Start(_cancellationToken);
}
try
{
_hostStorage.LoadShaders(_context, this);
}
catch (DiskCacheLoadException diskCacheLoadException)
{
Logger.Warning?.Print(LogClass.Gpu, $"Error loading the shader cache. {diskCacheLoadException.Message}");
// If we can't even access the file, then we also can't rebuild.
if (diskCacheLoadException.Result != DiskCacheLoadResult.NoAccess)
{
_needsHostRegen = true;
}
}
catch (InvalidDataException invalidDataException)
{
Logger.Warning?.Print(LogClass.Gpu, $"Error decompressing the shader cache file. {invalidDataException.Message}");
_needsHostRegen = true;
}
catch (IOException ioException)
{
Logger.Warning?.Print(LogClass.Gpu, $"Error reading the shader cache file. {ioException.Message}");
_needsHostRegen = true;
}
_asyncTranslationQueue.CompleteAdding();
for (int index = 0; index < ThreadCount; index++)
{
workThreads[index].Join();
}
CheckCompilationBlocking();
if (_needsHostRegen)
{
// Rebuild both shared and host cache files.
// Rebuilding shared is required because the shader information returned by the translator
// might have changed, and so we have to reconstruct the file with the new information.
try
{
_hostStorage.ClearSharedCache();
_hostStorage.ClearHostCache(_context);
if (_programList.Count != 0)
{
Logger.Info?.Print(LogClass.Gpu, $"Rebuilding {_programList.Count} shaders...");
using var streams = _hostStorage.GetOutputStreams(_context);
foreach (var kv in _programList)
{
if (!Active)
{
break;
}
CachedShaderProgram program = kv.Value;
_hostStorage.AddShader(_context, program, program.HostProgram.GetBinary(), streams);
}
Logger.Info?.Print(LogClass.Gpu, $"Rebuilt {_programList.Count} shaders successfully.");
}
else
{
_hostStorage.ClearGuestCache();
Logger.Info?.Print(LogClass.Gpu, "Shader cache deleted due to corruption.");
}
}
catch (DiskCacheLoadException diskCacheLoadException)
{
Logger.Warning?.Print(LogClass.Gpu, $"Error deleting the shader cache. {diskCacheLoadException.Message}");
}
catch (IOException ioException)
{
Logger.Warning?.Print(LogClass.Gpu, $"Error deleting the shader cache file. {ioException.Message}");
}
}
Logger.Info?.Print(LogClass.Gpu, "Shader cache loaded.");
_stateChangeCallback(ShaderCacheState.Loaded, programCount, programCount);
}
/// <summary>
/// Enqueues a host program for compilation.
/// </summary>
/// <param name="cachedProgram">Cached program</param>
/// <param name="hostProgram">Host program to be compiled</param>
/// <param name="programIndex">Program index</param>
/// <param name="isCompute">Indicates if the program is a compute shader</param>
public void QueueHostProgram(CachedShaderProgram cachedProgram, IProgram hostProgram, int programIndex, bool isCompute)
{
EnqueueForValidation(new ProgramEntry(cachedProgram, hostProgram, programIndex, isCompute, isBinary: true));
}
/// <summary>
/// Enqueues a guest program for compilation.
/// </summary>
/// <param name="shaders">Cached shader stages</param>
/// <param name="specState">Specialization state</param>
/// <param name="programIndex">Program index</param>
/// <param name="isCompute">Indicates if the program is a compute shader</param>
public void QueueGuestProgram(CachedShaderStage[] shaders, ShaderSpecializationState specState, int programIndex, bool isCompute)
{
_asyncTranslationQueue.Add(new AsyncProgramTranslation(shaders, specState, programIndex, isCompute));
}
/// <summary>
/// Check the state of programs that have already been compiled,
/// and add to the cache if the compilation was successful.
/// </summary>
public void CheckCompilation()
{
ProcessCompilationQueue();
// Process programs that already finished compiling.
// If not yet compiled, do nothing. This avoids blocking to wait for shader compilation.
while (_validationQueue.TryPeek(out ProgramEntry entry))
{
ProgramLinkStatus result = entry.HostProgram.CheckProgramLink(false);
if (result != ProgramLinkStatus.Incomplete)
{
ProcessCompiledProgram(ref entry, result);
_validationQueue.Dequeue();
}
else
{
break;
}
}
}
/// <summary>
/// Waits until all programs finishes compiling, then adds the ones
/// with successful compilation to the cache.
/// </summary>
private void CheckCompilationBlocking()
{
ProcessCompilationQueue();
while (_validationQueue.TryDequeue(out ProgramEntry entry) && Active)
{
ProcessCompiledProgram(ref entry, entry.HostProgram.CheckProgramLink(true), asyncCompile: false);
}
}
/// <summary>
/// Process a compiled program result.
/// </summary>
/// <param name="entry">Compiled program entry</param>
/// <param name="result">Compilation result</param>
/// <param name="asyncCompile">For failed host compilations, indicates if a guest compilation should be done asynchronously</param>
private void ProcessCompiledProgram(ref ProgramEntry entry, ProgramLinkStatus result, bool asyncCompile = true)
{
if (result == ProgramLinkStatus.Success)
{
// Compilation successful, add to memory cache.
if (entry.IsCompute)
{
_computeCache.Add(entry.CachedProgram);
}
else
{
_graphicsCache.Add(entry.CachedProgram);
}
if (!entry.IsBinary)
{
_needsHostRegen = true;
}
_programList.Add(entry.ProgramIndex, entry.CachedProgram);
SignalCompiled();
}
else if (entry.IsBinary)
{
// If this is a host binary and compilation failed,
// we still have a chance to recompile from the guest binary.
CachedShaderProgram program = entry.CachedProgram;
if (asyncCompile)
{
QueueGuestProgram(program.Shaders, program.SpecializationState, entry.ProgramIndex, entry.IsCompute);
}
else
{
RecompileFromGuestCode(program.Shaders, program.SpecializationState, entry.ProgramIndex, entry.IsCompute);
ProcessCompilationQueue();
}
}
else
{
// Failed to compile from both host and guest binary.
ErrorCount++;
SignalCompiled();
}
}
/// <summary>
/// Processes the queue of translated guest programs that should be compiled on the host.
/// </summary>
private void ProcessCompilationQueue()
{
while (_compilationQueue.TryDequeue(out ProgramCompilation compilation) && Active)
{
ShaderSource[] shaderSources = new ShaderSource[compilation.TranslatedStages.Length];
int fragmentOutputMap = -1;
for (int index = 0; index < compilation.TranslatedStages.Length; index++)
{
ShaderProgram shader = compilation.TranslatedStages[index];
shaderSources[index] = CreateShaderSource(shader);
if (shader.Info.Stage == ShaderStage.Fragment)
{
fragmentOutputMap = shader.Info.FragmentOutputMap;
}
}
IProgram hostProgram = _context.Renderer.CreateProgram(shaderSources, new ShaderInfo(fragmentOutputMap));
CachedShaderProgram program = new CachedShaderProgram(hostProgram, compilation.SpecializationState, compilation.Shaders);
EnqueueForValidation(new ProgramEntry(program, hostProgram, compilation.ProgramIndex, compilation.IsCompute, isBinary: false));
}
}
/// <summary>
/// Enqueues a program for validation, which will check if the program was compiled successfully.
/// </summary>
/// <param name="newEntry">Program entry to be validated</param>
private void EnqueueForValidation(ProgramEntry newEntry)
{
_validationQueue.Enqueue(newEntry);
// Do not allow more than N shader compilation in-flight, where N is the maximum number of threads
// the driver will be using for parallel compilation.
// Submitting more seems to cause NVIDIA OpenGL driver to crash.
if (_validationQueue.Count >= _backendParallelCompileThreads && _validationQueue.TryDequeue(out ProgramEntry entry))
{
ProcessCompiledProgram(ref entry, entry.HostProgram.CheckProgramLink(true), asyncCompile: false);
}
}
/// <summary>
/// Processses the queue of programs that should be translated from guest code.
/// </summary>
/// <param name="state">Cancellation token</param>
private void ProcessAsyncQueue(object state)
{
CancellationToken ct = (CancellationToken)state;
try
{
foreach (AsyncProgramTranslation asyncCompilation in _asyncTranslationQueue.GetConsumingEnumerable(ct))
{
RecompileFromGuestCode(
asyncCompilation.Shaders,
asyncCompilation.SpecializationState,
asyncCompilation.ProgramIndex,
asyncCompilation.IsCompute);
}
}
catch (OperationCanceledException)
{
}
}
/// <summary>
/// Recompiles a program from guest code.
/// </summary>
/// <param name="shaders">Shader stages</param>
/// <param name="specState">Specialization state</param>
/// <param name="programIndex">Program index</param>
/// <param name="isCompute">Indicates if the program is a compute shader</param>
private void RecompileFromGuestCode(CachedShaderStage[] shaders, ShaderSpecializationState specState, int programIndex, bool isCompute)
{
try
{
if (isCompute)
{
RecompileComputeFromGuestCode(shaders, specState, programIndex);
}
else
{
RecompileGraphicsFromGuestCode(shaders, specState, programIndex);
}
}
catch (DiskCacheLoadException diskCacheLoadException)
{
Logger.Error?.Print(LogClass.Gpu, $"Error translating guest shader. {diskCacheLoadException.Message}");
ErrorCount++;
SignalCompiled();
}
}
/// <summary>
/// Recompiles a graphics program from guest code.
/// </summary>
/// <param name="shaders">Shader stages</param>
/// <param name="specState">Specialization state</param>
/// <param name="programIndex">Program index</param>
private void RecompileGraphicsFromGuestCode(CachedShaderStage[] shaders, ShaderSpecializationState specState, int programIndex)
{
ShaderSpecializationState newSpecState = new ShaderSpecializationState(specState.GraphicsState, specState.TransformFeedbackDescriptors);
ResourceCounts counts = new ResourceCounts();
TranslatorContext[] translatorContexts = new TranslatorContext[Constants.ShaderStages + 1];
TranslatorContext nextStage = null;
for (int stageIndex = Constants.ShaderStages - 1; stageIndex >= 0; stageIndex--)
{
CachedShaderStage shader = shaders[stageIndex + 1];
if (shader != null)
{
byte[] guestCode = shader.Code;
byte[] cb1Data = shader.Cb1Data;
DiskCacheGpuAccessor gpuAccessor = new DiskCacheGpuAccessor(_context, guestCode, cb1Data, specState, newSpecState, counts, stageIndex);
TranslatorContext currentStage = DecodeGraphicsShader(gpuAccessor, DefaultFlags, 0);
if (nextStage != null)
{
currentStage.SetNextStage(nextStage);
}
if (stageIndex == 0 && shaders[0] != null)
{
byte[] guestCodeA = shaders[0].Code;
byte[] cb1DataA = shaders[0].Cb1Data;
DiskCacheGpuAccessor gpuAccessorA = new DiskCacheGpuAccessor(_context, guestCodeA, cb1DataA, specState, newSpecState, counts, 0);
translatorContexts[0] = DecodeGraphicsShader(gpuAccessorA, DefaultFlags | TranslationFlags.VertexA, 0);
}
translatorContexts[stageIndex + 1] = currentStage;
nextStage = currentStage;
}
}
List<ShaderProgram> translatedStages = new List<ShaderProgram>();
for (int stageIndex = 0; stageIndex < Constants.ShaderStages; stageIndex++)
{
TranslatorContext currentStage = translatorContexts[stageIndex + 1];
if (currentStage != null)
{
ShaderProgram program;
byte[] guestCode = shaders[stageIndex + 1].Code;
byte[] cb1Data = shaders[stageIndex + 1].Cb1Data;
if (stageIndex == 0 && shaders[0] != null)
{
program = currentStage.Translate(translatorContexts[0]);
byte[] guestCodeA = shaders[0].Code;
byte[] cb1DataA = shaders[0].Cb1Data;
shaders[0] = new CachedShaderStage(null, guestCodeA, cb1DataA);
shaders[1] = new CachedShaderStage(program.Info, guestCode, cb1Data);
}
else
{
program = currentStage.Translate();
shaders[stageIndex + 1] = new CachedShaderStage(program.Info, guestCode, cb1Data);
}
if (program != null)
{
translatedStages.Add(program);
}
}
}
_compilationQueue.Enqueue(new ProgramCompilation(translatedStages.ToArray(), shaders, newSpecState, programIndex, isCompute: false));
}
/// <summary>
/// Recompiles a compute program from guest code.
/// </summary>
/// <param name="shaders">Shader stages</param>
/// <param name="specState">Specialization state</param>
/// <param name="programIndex">Program index</param>
private void RecompileComputeFromGuestCode(CachedShaderStage[] shaders, ShaderSpecializationState specState, int programIndex)
{
CachedShaderStage shader = shaders[0];
ResourceCounts counts = new ResourceCounts();
ShaderSpecializationState newSpecState = new ShaderSpecializationState(specState.ComputeState);
DiskCacheGpuAccessor gpuAccessor = new DiskCacheGpuAccessor(_context, shader.Code, shader.Cb1Data, specState, newSpecState, counts, 0);
TranslatorContext translatorContext = DecodeComputeShader(gpuAccessor, 0);
ShaderProgram program = translatorContext.Translate();
shaders[0] = new CachedShaderStage(program.Info, shader.Code, shader.Cb1Data);
_compilationQueue.Enqueue(new ProgramCompilation(new[] { program }, shaders, newSpecState, programIndex, isCompute: true));
}
/// <summary>
/// Signals that compilation of a program has been finished successfully,
/// or that it failed and guest recompilation has also been attempted.
/// </summary>
private void SignalCompiled()
{
_stateChangeCallback(ShaderCacheState.Loading, ++_compiledCount, _totalCount);
}
}
}

View file

@ -1,5 +1,5 @@
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Image;
using Ryujinx.Graphics.Shader;
using System;
using System.Runtime.InteropServices;
@ -9,19 +9,12 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// <summary>
/// Represents a GPU state and memory accessor.
/// </summary>
class GpuAccessor : TextureDescriptorCapableGpuAccessor, IGpuAccessor
class GpuAccessor : GpuAccessorBase, IGpuAccessor
{
private readonly GpuChannel _channel;
private readonly GpuAccessorState _state;
private readonly int _stageIndex;
private readonly bool _compute;
private readonly int _localSizeX;
private readonly int _localSizeY;
private readonly int _localSizeZ;
private readonly int _localMemorySize;
private readonly int _sharedMemorySize;
public int Cb1DataSize { get; private set; }
/// <summary>
/// Creates a new instance of the GPU state accessor for graphics shader translation.
@ -43,43 +36,16 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// <param name="context">GPU context</param>
/// <param name="channel">GPU channel</param>
/// <param name="state">Current GPU state</param>
/// <param name="localSizeX">Local group size X of the compute shader</param>
/// <param name="localSizeY">Local group size Y of the compute shader</param>
/// <param name="localSizeZ">Local group size Z of the compute shader</param>
/// <param name="localMemorySize">Local memory size of the compute shader</param>
/// <param name="sharedMemorySize">Shared memory size of the compute shader</param>
public GpuAccessor(
GpuContext context,
GpuChannel channel,
GpuAccessorState state,
int localSizeX,
int localSizeY,
int localSizeZ,
int localMemorySize,
int sharedMemorySize) : base(context)
public GpuAccessor(GpuContext context, GpuChannel channel, GpuAccessorState state) : base(context)
{
_channel = channel;
_state = state;
_compute = true;
_localSizeX = localSizeX;
_localSizeY = localSizeY;
_localSizeZ = localSizeZ;
_localMemorySize = localMemorySize;
_sharedMemorySize = sharedMemorySize;
}
/// <summary>
/// Reads data from the constant buffer 1.
/// </summary>
/// <param name="offset">Offset in bytes to read from</param>
/// <returns>Value at the given offset</returns>
/// <inheritdoc/>
public uint ConstantBuffer1Read(int offset)
{
if (Cb1DataSize < offset + 4)
{
Cb1DataSize = offset + 4;
}
ulong baseAddress = _compute
? _channel.BufferManager.GetComputeUniformBufferAddress(1)
: _channel.BufferManager.GetGraphicsUniformBufferAddress(_stageIndex, 1);
@ -87,111 +53,115 @@ namespace Ryujinx.Graphics.Gpu.Shader
return _channel.MemoryManager.Physical.Read<uint>(baseAddress + (ulong)offset);
}
/// <summary>
/// Prints a log message.
/// </summary>
/// <param name="message">Message to print</param>
/// <inheritdoc/>
public void Log(string message)
{
Logger.Warning?.Print(LogClass.Gpu, $"Shader translator: {message}");
}
/// <summary>
/// Gets a span of the specified memory location, containing shader code.
/// </summary>
/// <param name="address">GPU virtual address of the data</param>
/// <param name="minimumSize">Minimum size that the returned span may have</param>
/// <returns>Span of the memory location</returns>
public override ReadOnlySpan<ulong> GetCode(ulong address, int minimumSize)
/// <inheritdoc/>
public ReadOnlySpan<ulong> GetCode(ulong address, int minimumSize)
{
int size = Math.Max(minimumSize, 0x1000 - (int)(address & 0xfff));
return MemoryMarshal.Cast<byte, ulong>(_channel.MemoryManager.GetSpan(address, size));
}
/// <summary>
/// Queries Local Size X for compute shaders.
/// </summary>
/// <returns>Local Size X</returns>
public int QueryComputeLocalSizeX() => _localSizeX;
/// <inheritdoc/>
public int QueryBindingConstantBuffer(int index)
{
return _state.ResourceCounts.UniformBuffersCount++;
}
/// <summary>
/// Queries Local Size Y for compute shaders.
/// </summary>
/// <returns>Local Size Y</returns>
public int QueryComputeLocalSizeY() => _localSizeY;
/// <inheritdoc/>
public int QueryBindingStorageBuffer(int index)
{
return _state.ResourceCounts.StorageBuffersCount++;
}
/// <summary>
/// Queries Local Size Z for compute shaders.
/// </summary>
/// <returns>Local Size Z</returns>
public int QueryComputeLocalSizeZ() => _localSizeZ;
/// <inheritdoc/>
public int QueryBindingTexture(int index)
{
return _state.ResourceCounts.TexturesCount++;
}
/// <summary>
/// Queries Local Memory size in bytes for compute shaders.
/// </summary>
/// <returns>Local Memory size in bytes</returns>
public int QueryComputeLocalMemorySize() => _localMemorySize;
/// <inheritdoc/>
public int QueryBindingImage(int index)
{
return _state.ResourceCounts.ImagesCount++;
}
/// <summary>
/// Queries Shared Memory size in bytes for compute shaders.
/// </summary>
/// <returns>Shared Memory size in bytes</returns>
public int QueryComputeSharedMemorySize() => _sharedMemorySize;
/// <inheritdoc/>
public int QueryComputeLocalSizeX() => _state.ComputeState.LocalSizeX;
/// <summary>
/// Queries Constant Buffer usage information.
/// </summary>
/// <returns>A mask where each bit set indicates a bound constant buffer</returns>
/// <inheritdoc/>
public int QueryComputeLocalSizeY() => _state.ComputeState.LocalSizeY;
/// <inheritdoc/>
public int QueryComputeLocalSizeZ() => _state.ComputeState.LocalSizeZ;
/// <inheritdoc/>
public int QueryComputeLocalMemorySize() => _state.ComputeState.LocalMemorySize;
/// <inheritdoc/>
public int QueryComputeSharedMemorySize() => _state.ComputeState.SharedMemorySize;
/// <inheritdoc/>
public uint QueryConstantBufferUse()
{
return _compute
uint useMask = _compute
? _channel.BufferManager.GetComputeUniformBufferUseMask()
: _channel.BufferManager.GetGraphicsUniformBufferUseMask(_stageIndex);
_state.SpecializationState?.RecordConstantBufferUse(_stageIndex, useMask);
return useMask;
}
/// <summary>
/// Queries current primitive topology for geometry shaders.
/// </summary>
/// <returns>Current primitive topology</returns>
/// <inheritdoc/>
public InputTopology QueryPrimitiveTopology()
{
return _state.Topology switch
{
PrimitiveTopology.Points => InputTopology.Points,
PrimitiveTopology.Lines or
PrimitiveTopology.LineLoop or
PrimitiveTopology.LineStrip => InputTopology.Lines,
PrimitiveTopology.LinesAdjacency or
PrimitiveTopology.LineStripAdjacency => InputTopology.LinesAdjacency,
PrimitiveTopology.Triangles or
PrimitiveTopology.TriangleStrip or
PrimitiveTopology.TriangleFan => InputTopology.Triangles,
PrimitiveTopology.TrianglesAdjacency or
PrimitiveTopology.TriangleStripAdjacency => InputTopology.TrianglesAdjacency,
PrimitiveTopology.Patches => _state.TessellationMode.UnpackPatchType() == TessPatchType.Isolines
? InputTopology.Lines
: InputTopology.Triangles,
_ => InputTopology.Points
};
_state.SpecializationState?.RecordPrimitiveTopology();
return ConvertToInputTopology(_state.GraphicsState.Topology, _state.GraphicsState.TessellationMode);
}
/// <summary>
/// Queries the tessellation evaluation shader primitive winding order.
/// </summary>
/// <returns>True if the primitive winding order is clockwise, false if counter-clockwise</returns>
public bool QueryTessCw() => _state.TessellationMode.UnpackCw();
/// <inheritdoc/>
public bool QueryTessCw()
{
return _state.GraphicsState.TessellationMode.UnpackCw();
}
/// <summary>
/// Queries the tessellation evaluation shader abstract patch type.
/// </summary>
/// <returns>Abstract patch type</returns>
public TessPatchType QueryTessPatchType() => _state.TessellationMode.UnpackPatchType();
/// <inheritdoc/>
public TessPatchType QueryTessPatchType()
{
return _state.GraphicsState.TessellationMode.UnpackPatchType();
}
/// <summary>
/// Queries the tessellation evaluation shader spacing between tessellated vertices of the patch.
/// </summary>
/// <returns>Spacing between tessellated vertices of the patch</returns>
public TessSpacing QueryTessSpacing() => _state.TessellationMode.UnpackSpacing();
/// <inheritdoc/>
public TessSpacing QueryTessSpacing()
{
return _state.GraphicsState.TessellationMode.UnpackSpacing();
}
//// <inheritdoc/>
public TextureFormat QueryTextureFormat(int handle, int cbufSlot)
{
_state.SpecializationState?.RecordTextureFormat(_stageIndex, handle, cbufSlot);
var descriptor = GetTextureDescriptor(handle, cbufSlot);
return ConvertToTextureFormat(descriptor.UnpackFormat(), descriptor.UnpackSrgb());
}
/// <inheritdoc/>
public SamplerType QuerySamplerType(int handle, int cbufSlot)
{
_state.SpecializationState?.RecordTextureSamplerType(_stageIndex, handle, cbufSlot);
return GetTextureDescriptor(handle, cbufSlot).UnpackTextureTarget().ConvertSamplerType();
}
/// <inheritdoc/>
public bool QueryTextureCoordNormalized(int handle, int cbufSlot)
{
_state.SpecializationState?.RecordTextureCoordNormalized(_stageIndex, handle, cbufSlot);
return GetTextureDescriptor(handle, cbufSlot).UnpackTextureCoordNormalized();
}
/// <summary>
/// Gets the texture descriptor for a given texture on the pool.
@ -199,65 +169,58 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// <param name="handle">Index of the texture (this is the word offset of the handle in the constant buffer)</param>
/// <param name="cbufSlot">Constant buffer slot for the texture handle</param>
/// <returns>Texture descriptor</returns>
public override Image.ITextureDescriptor GetTextureDescriptor(int handle, int cbufSlot)
private Image.TextureDescriptor GetTextureDescriptor(int handle, int cbufSlot)
{
if (_compute)
{
return _channel.TextureManager.GetComputeTextureDescriptor(
_state.TexturePoolGpuVa,
_state.TextureBufferIndex,
_state.TexturePoolMaximumId,
_state.PoolState.TexturePoolGpuVa,
_state.PoolState.TextureBufferIndex,
_state.PoolState.TexturePoolMaximumId,
handle,
cbufSlot);
}
else
{
return _channel.TextureManager.GetGraphicsTextureDescriptor(
_state.TexturePoolGpuVa,
_state.TextureBufferIndex,
_state.TexturePoolMaximumId,
_state.PoolState.TexturePoolGpuVa,
_state.PoolState.TextureBufferIndex,
_state.PoolState.TexturePoolMaximumId,
_stageIndex,
handle,
cbufSlot);
}
}
/// <summary>
/// Queries transform feedback enable state.
/// </summary>
/// <returns>True if the shader uses transform feedback, false otherwise</returns>
/// <inheritdoc/>
public bool QueryTransformFeedbackEnabled()
{
return _state.TransformFeedbackDescriptors != null;
}
/// <summary>
/// Queries the varying locations that should be written to the transform feedback buffer.
/// </summary>
/// <param name="bufferIndex">Index of the transform feedback buffer</param>
/// <returns>Varying locations for the specified buffer</returns>
/// <inheritdoc/>
public ReadOnlySpan<byte> QueryTransformFeedbackVaryingLocations(int bufferIndex)
{
return _state.TransformFeedbackDescriptors[bufferIndex].VaryingLocations;
return _state.TransformFeedbackDescriptors[bufferIndex].AsSpan();
}
/// <summary>
/// Queries the stride (in bytes) of the per vertex data written into the transform feedback buffer.
/// </summary>
/// <param name="bufferIndex">Index of the transform feedback buffer</param>
/// <returns>Stride for the specified buffer</returns>
/// <inheritdoc/>
public int QueryTransformFeedbackStride(int bufferIndex)
{
return _state.TransformFeedbackDescriptors[bufferIndex].Stride;
}
/// <summary>
/// Queries if host state forces early depth testing.
/// </summary>
/// <returns>True if early depth testing is forced</returns>
/// <inheritdoc/>
public bool QueryEarlyZForce()
{
return _state.EarlyZForce;
_state.SpecializationState?.RecordEarlyZForce();
return _state.GraphicsState.EarlyZForce;
}
/// <inheritdoc/>
public void RegisterTexture(int handle, int cbufSlot)
{
_state.SpecializationState?.RegisterTexture(_stageIndex, handle, cbufSlot, GetTextureDescriptor(handle, cbufSlot));
}
}
}

View file

@ -1,23 +1,26 @@
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Engine.Threed;
using Ryujinx.Graphics.Gpu.Image;
using Ryujinx.Graphics.Shader;
using System;
namespace Ryujinx.Graphics.Gpu.Shader
{
abstract class TextureDescriptorCapableGpuAccessor : IGpuAccessor
/// <summary>
/// GPU accessor.
/// </summary>
class GpuAccessorBase
{
private readonly GpuContext _context;
public TextureDescriptorCapableGpuAccessor(GpuContext context)
/// <summary>
/// Creates a new GPU accessor.
/// </summary>
/// <param name="context">GPU context</param>
public GpuAccessorBase(GpuContext context)
{
_context = context;
}
public abstract ReadOnlySpan<ulong> GetCode(ulong address, int minimumSize);
public abstract ITextureDescriptor GetTextureDescriptor(int handle, int cbufSlot);
/// <summary>
/// Queries host about the presence of the FrontFacing built-in variable bug.
/// </summary>
@ -79,20 +82,14 @@ namespace Ryujinx.Graphics.Gpu.Shader
public bool QueryHostSupportsTextureShadowLod() => _context.Capabilities.SupportsTextureShadowLod;
/// <summary>
/// Queries texture format information, for shaders using image load or store.
/// Converts a packed Maxwell texture format to the shader translator texture format.
/// </summary>
/// <remarks>
/// This only returns non-compressed color formats.
/// If the format of the texture is a compressed, depth or unsupported format, then a default value is returned.
/// </remarks>
/// <param name="handle">Texture handle</param>
/// <param name="cbufSlot">Constant buffer slot for the texture handle</param>
/// <returns>Color format of the non-compressed texture</returns>
public TextureFormat QueryTextureFormat(int handle, int cbufSlot = -1)
/// <param name="format">Packed maxwell format</param>
/// <param name="formatSrgb">Indicates if the format is sRGB</param>
/// <returns>Shader translator texture format</returns>
protected static TextureFormat ConvertToTextureFormat(uint format, bool formatSrgb)
{
var descriptor = GetTextureDescriptor(handle, cbufSlot);
if (!FormatTable.TryGetTextureFormat(descriptor.UnpackFormat(), descriptor.UnpackSrgb(), out FormatInfo formatInfo))
if (!FormatTable.TryGetTextureFormat(format, formatSrgb, out FormatInfo formatInfo))
{
return TextureFormat.Unknown;
}
@ -144,32 +141,31 @@ namespace Ryujinx.Graphics.Gpu.Shader
}
/// <summary>
/// Queries sampler type information.
/// Converts the Maxwell primitive topology to the shader translator topology.
/// </summary>
/// <param name="handle">Texture handle</param>
/// <param name="cbufSlot">Constant buffer slot for the texture handle</param>
/// <returns>The sampler type value for the given handle</returns>
public SamplerType QuerySamplerType(int handle, int cbufSlot = -1)
/// <param name="topology">Maxwell primitive topology</param>
/// <param name="tessellationMode">Maxwell tessellation mode</param>
/// <returns>Shader translator topology</returns>
protected static InputTopology ConvertToInputTopology(PrimitiveTopology topology, TessMode tessellationMode)
{
return GetTextureDescriptor(handle, cbufSlot).UnpackTextureTarget().ConvertSamplerType();
}
/// <summary>
/// Queries texture target information.
/// </summary>
/// <param name="handle">Texture handle</param>
/// <param name="cbufSlot">Constant buffer slot for the texture handle</param>
/// <returns>True if the texture is a rectangle texture, false otherwise</returns>
public bool QueryIsTextureRectangle(int handle, int cbufSlot = -1)
{
var descriptor = GetTextureDescriptor(handle, cbufSlot);
TextureTarget target = descriptor.UnpackTextureTarget();
bool is2DTexture = target == TextureTarget.Texture2D ||
target == TextureTarget.Texture2DRect;
return !descriptor.UnpackTextureCoordNormalized() && is2DTexture;
return topology switch
{
PrimitiveTopology.Points => InputTopology.Points,
PrimitiveTopology.Lines or
PrimitiveTopology.LineLoop or
PrimitiveTopology.LineStrip => InputTopology.Lines,
PrimitiveTopology.LinesAdjacency or
PrimitiveTopology.LineStripAdjacency => InputTopology.LinesAdjacency,
PrimitiveTopology.Triangles or
PrimitiveTopology.TriangleStrip or
PrimitiveTopology.TriangleFan => InputTopology.Triangles,
PrimitiveTopology.TrianglesAdjacency or
PrimitiveTopology.TriangleStripAdjacency => InputTopology.TrianglesAdjacency,
PrimitiveTopology.Patches => tessellationMode.UnpackPatchType() == TessPatchType.Isolines
? InputTopology.Lines
: InputTopology.Triangles,
_ => InputTopology.Points
};
}
}
}

View file

@ -1,72 +1,61 @@
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Engine.Threed;
namespace Ryujinx.Graphics.Gpu.Shader
{
/// <summary>
/// State used by the <see cref="GpuAccessor"/>.
/// </summary>
struct GpuAccessorState
class GpuAccessorState
{
/// <summary>
/// GPU virtual address of the texture pool.
/// GPU texture pool state.
/// </summary>
public ulong TexturePoolGpuVa { get; }
public readonly GpuChannelPoolState PoolState;
/// <summary>
/// Maximum ID of the texture pool.
/// GPU compute state, for compute shaders.
/// </summary>
public int TexturePoolMaximumId { get; }
public readonly GpuChannelComputeState ComputeState;
/// <summary>
/// Constant buffer slot where the texture handles are located.
/// GPU graphics state, for vertex, tessellation, geometry and fragment shaders.
/// </summary>
public int TextureBufferIndex { get; }
public readonly GpuChannelGraphicsState GraphicsState;
/// <summary>
/// Early Z force enable.
/// Shader specialization state (shared by all stages).
/// </summary>
public bool EarlyZForce { get; }
/// <summary>
/// Primitive topology of current draw.
/// </summary>
public PrimitiveTopology Topology { get; }
/// <summary>
/// Tessellation mode.
/// </summary>
public TessMode TessellationMode { get; }
public readonly ShaderSpecializationState SpecializationState;
/// <summary>
/// Transform feedback information, if the shader uses transform feedback. Otherwise, should be null.
/// </summary>
public TransformFeedbackDescriptor[] TransformFeedbackDescriptors { get; set; }
public readonly TransformFeedbackDescriptor[] TransformFeedbackDescriptors;
/// <summary>
/// Creates a new instance of the GPU accessor state.
/// Shader resource counts (shared by all stages).
/// </summary>
/// <param name="texturePoolGpuVa">GPU virtual address of the texture pool</param>
/// <param name="texturePoolMaximumId">Maximum ID of the texture pool</param>
/// <param name="textureBufferIndex">Constant buffer slot where the texture handles are located</param>
/// <param name="earlyZForce">Early Z force enable</param>
/// <param name="topology">Primitive topology</param>
/// <param name="tessellationMode">Tessellation mode</param>
public readonly ResourceCounts ResourceCounts;
/// <summary>
/// Creates a new GPU accessor state.
/// </summary>
/// <param name="poolState">GPU texture pool state</param>
/// <param name="computeState">GPU compute state, for compute shaders</param>
/// <param name="graphicsState">GPU graphics state, for vertex, tessellation, geometry and fragment shaders</param>
/// <param name="specializationState">Shader specialization state (shared by all stages)</param>
/// <param name="transformFeedbackDescriptors">Transform feedback information, if the shader uses transform feedback. Otherwise, should be null</param>
public GpuAccessorState(
ulong texturePoolGpuVa,
int texturePoolMaximumId,
int textureBufferIndex,
bool earlyZForce,
PrimitiveTopology topology,
TessMode tessellationMode)
GpuChannelPoolState poolState,
GpuChannelComputeState computeState,
GpuChannelGraphicsState graphicsState,
ShaderSpecializationState specializationState,
TransformFeedbackDescriptor[] transformFeedbackDescriptors = null)
{
TexturePoolGpuVa = texturePoolGpuVa;
TexturePoolMaximumId = texturePoolMaximumId;
TextureBufferIndex = textureBufferIndex;
EarlyZForce = earlyZForce;
Topology = topology;
TessellationMode = tessellationMode;
TransformFeedbackDescriptors = null;
PoolState = poolState;
GraphicsState = graphicsState;
ComputeState = computeState;
SpecializationState = specializationState;
TransformFeedbackDescriptors = transformFeedbackDescriptors;
ResourceCounts = new ResourceCounts();
}
}
}

View file

@ -0,0 +1,57 @@
namespace Ryujinx.Graphics.Gpu.Shader
{
/// <summary>
/// State used by the <see cref="GpuAccessor"/>.
/// </summary>
struct GpuChannelComputeState
{
// New fields should be added to the end of the struct to keep disk shader cache compatibility.
/// <summary>
/// Local group size X of the compute shader.
/// </summary>
public readonly int LocalSizeX;
/// <summary>
/// Local group size Y of the compute shader.
/// </summary>
public readonly int LocalSizeY;
/// <summary>
/// Local group size Z of the compute shader.
/// </summary>
public readonly int LocalSizeZ;
/// <summary>
/// Local memory size of the compute shader.
/// </summary>
public readonly int LocalMemorySize;
/// <summary>
/// Shared memory size of the compute shader.
/// </summary>
public readonly int SharedMemorySize;
/// <summary>
/// Creates a new GPU compute state.
/// </summary>
/// <param name="localSizeX">Local group size X of the compute shader</param>
/// <param name="localSizeY">Local group size Y of the compute shader</param>
/// <param name="localSizeZ">Local group size Z of the compute shader</param>
/// <param name="localMemorySize">Local memory size of the compute shader</param>
/// <param name="sharedMemorySize">Shared memory size of the compute shader</param>
public GpuChannelComputeState(
int localSizeX,
int localSizeY,
int localSizeZ,
int localMemorySize,
int sharedMemorySize)
{
LocalSizeX = localSizeX;
LocalSizeY = localSizeY;
LocalSizeZ = localSizeZ;
LocalMemorySize = localMemorySize;
SharedMemorySize = sharedMemorySize;
}
}
}

View file

@ -0,0 +1,41 @@
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Engine.Threed;
namespace Ryujinx.Graphics.Gpu.Shader
{
/// <summary>
/// State used by the <see cref="GpuAccessor"/>.
/// </summary>
struct GpuChannelGraphicsState
{
// New fields should be added to the end of the struct to keep disk shader cache compatibility.
/// <summary>
/// Early Z force enable.
/// </summary>
public readonly bool EarlyZForce;
/// <summary>
/// Primitive topology of current draw.
/// </summary>
public readonly PrimitiveTopology Topology;
/// <summary>
/// Tessellation mode.
/// </summary>
public readonly TessMode TessellationMode;
/// <summary>
/// Creates a new GPU graphics state.
/// </summary>
/// <param name="earlyZForce">Early Z force enable</param>
/// <param name="topology">Primitive topology</param>
/// <param name="tessellationMode">Tessellation mode</param>
public GpuChannelGraphicsState(bool earlyZForce, PrimitiveTopology topology, TessMode tessellationMode)
{
EarlyZForce = earlyZForce;
Topology = topology;
TessellationMode = tessellationMode;
}
}
}

View file

@ -0,0 +1,36 @@
namespace Ryujinx.Graphics.Gpu.Shader
{
/// <summary>
/// State used by the <see cref="GpuAccessor"/>.
/// </summary>
struct GpuChannelPoolState
{
/// <summary>
/// GPU virtual address of the texture pool.
/// </summary>
public readonly ulong TexturePoolGpuVa;
/// <summary>
/// Maximum ID of the texture pool.
/// </summary>
public readonly int TexturePoolMaximumId;
/// <summary>
/// Constant buffer slot where the texture handles are located.
/// </summary>
public readonly int TextureBufferIndex;
/// <summary>
/// Creates a new GPU texture pool state.
/// </summary>
/// <param name="texturePoolGpuVa">GPU virtual address of the texture pool</param>
/// <param name="texturePoolMaximumId">Maximum ID of the texture pool</param>
/// <param name="textureBufferIndex">Constant buffer slot where the texture handles are located</param>
public GpuChannelPoolState(ulong texturePoolGpuVa, int texturePoolMaximumId, int textureBufferIndex)
{
TexturePoolGpuVa = texturePoolGpuVa;
TexturePoolMaximumId = texturePoolMaximumId;
TextureBufferIndex = textureBufferIndex;
}
}
}

View file

@ -0,0 +1,113 @@
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Shader.HashTable
{
/// <summary>
/// State of a hash calculation.
/// </summary>
struct HashState
{
// This is using a slightly modified implementation of FastHash64.
// Reference: https://github.com/ztanml/fast-hash/blob/master/fasthash.c
private const ulong M = 0x880355f21e6d1965UL;
private ulong _hash;
private int _start;
/// <summary>
/// One shot hash calculation for a given data.
/// </summary>
/// <param name="data">Data to be hashed</param>
/// <returns>Hash of the given data</returns>
public static uint CalcHash(ReadOnlySpan<byte> data)
{
HashState state = new HashState();
state.Initialize();
state.Continue(data);
return state.Finalize(data);
}
/// <summary>
/// Initializes the hash state.
/// </summary>
public void Initialize()
{
_hash = 23;
}
/// <summary>
/// Calculates the hash of the given data.
/// </summary>
/// <remarks>
/// The full data must be passed on <paramref name="data"/>.
/// If this is not the first time the method is called, then <paramref name="data"/> must start with the data passed on the last call.
/// If a smaller slice of the data was already hashed before, only the additional data will be hashed.
/// This can be used for additive hashing of data in chuncks.
/// </remarks>
/// <param name="data">Data to be hashed</param>
public void Continue(ReadOnlySpan<byte> data)
{
ulong h = _hash;
ReadOnlySpan<ulong> dataAsUlong = MemoryMarshal.Cast<byte, ulong>(data.Slice(_start));
for (int i = 0; i < dataAsUlong.Length; i++)
{
ulong value = dataAsUlong[i];
h ^= Mix(value);
h *= M;
}
_hash = h;
_start = data.Length & ~7;
}
/// <summary>
/// Performs the hash finalization step, and returns the calculated hash.
/// </summary>
/// <remarks>
/// The full data must be passed on <paramref name="data"/>.
/// <paramref name="data"/> must start with the data passed on the last call to <see cref="Continue"/>.
/// No internal state is changed, so one can still continue hashing data with <see cref="Continue"/>
/// after calling this method.
/// </remarks>
/// <param name="data">Data to be hashed</param>
/// <returns>Hash of all the data hashed with this <see cref="HashState"/></returns>
public uint Finalize(ReadOnlySpan<byte> data)
{
ulong h = _hash;
int remainder = data.Length & 7;
if (remainder != 0)
{
ulong v = 0;
for (int i = data.Length - remainder; i < data.Length; i++)
{
v |= (ulong)data[i] << ((i - remainder) * 8);
}
h ^= Mix(v);
h *= M;
}
h = Mix(h);
return (uint)(h - (h >> 32));
}
/// <summary>
/// Hash mix function.
/// </summary>
/// <param name="h">Hash to mix</param>
/// <returns>Mixed hash</returns>
private static ulong Mix(ulong h)
{
h ^= h >> 23;
h *= 0x2127599bf4325c37UL;
h ^= h >> 47;
return h;
}
}
}

View file

@ -0,0 +1,27 @@
using System;
namespace Ryujinx.Graphics.Gpu.Shader.HashTable
{
/// <summary>
/// Data accessor, used by <see cref="PartitionedHashTable{T}"/> to access data of unknown length.
/// </summary>
/// <remarks>
/// This will be used to access chuncks of data and try finding a match on the table.
/// This is necessary because the data size is assumed to be unknown, and so the
/// hash table must try to "guess" the size of the data based on the entries on the table.
/// </remarks>
public interface IDataAccessor
{
/// <summary>
/// Gets a span of shader code at the specified offset, with at most the specified size.
/// </summary>
/// <remarks>
/// This might return a span smaller than the requested <paramref name="length"/> if there's
/// no more code available.
/// </remarks>
/// <param name="offset">Offset in shader code</param>
/// <param name="length">Size in bytes</param>
/// <returns>Shader code span</returns>
ReadOnlySpan<byte> GetSpan(int offset, int length);
}
}

View file

@ -0,0 +1,452 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace Ryujinx.Graphics.Gpu.Shader.HashTable
{
/// <summary>
/// Partitioned hash table.
/// </summary>
/// <typeparam name="T">Hash table entry type</typeparam>
class PartitionHashTable<T>
{
/// <summary>
/// Hash table entry.
/// </summary>
private struct Entry
{
/// <summary>
/// Hash <see cref="OwnSize"/> bytes of <see cref="Data"/>.
/// </summary>
public readonly uint Hash;
/// <summary>
/// If this entry is only a sub-region of <see cref="Data"/>, this indicates the size in bytes
/// of that region. Otherwise, it should be zero.
/// </summary>
public readonly int OwnSize;
/// <summary>
/// Data used to compute the hash for this entry.
/// </summary>
/// <remarks>
/// To avoid additional allocations, this might be a instance of the full entry data,
/// and only a sub-region of it might be actually used by this entry. Such sub-region
/// has its size indicated by <see cref="OwnSize"/> in this case.
/// </remarks>
public readonly byte[] Data;
/// <summary>
/// Item associated with this entry.
/// </summary>
public T Item;
/// <summary>
/// Indicates if the entry is partial, which means that this entry is only for a sub-region of the data.
/// </summary>
/// <remarks>
/// Partial entries have no items associated with them. They just indicates that the data might be present on
/// the table, and one must keep looking for the full entry on other tables of larger data size.
/// </remarks>
public bool IsPartial => OwnSize != 0;
/// <summary>
/// Creates a new partial hash table entry.
/// </summary>
/// <param name="hash">Hash of the data</param>
/// <param name="ownerData">Full data</param>
/// <param name="ownSize">Size of the sub-region of data that belongs to this entry</param>
public Entry(uint hash, byte[] ownerData, int ownSize)
{
Hash = hash;
OwnSize = ownSize;
Data = ownerData;
Item = default;
}
/// <summary>
/// Creates a new full hash table entry.
/// </summary>
/// <param name="hash">Hash of the data</param>
/// <param name="data">Data</param>
/// <param name="item">Item associated with this entry</param>
public Entry(uint hash, byte[] data, T item)
{
Hash = hash;
OwnSize = 0;
Data = data;
Item = item;
}
/// <summary>
/// Gets the data for this entry, either full or partial.
/// </summary>
/// <returns>Data sub-region</returns>
public ReadOnlySpan<byte> GetData()
{
if (OwnSize != 0)
{
return new ReadOnlySpan<byte>(Data).Slice(0, OwnSize);
}
return Data;
}
}
/// <summary>
/// Hash table bucket.
/// </summary>
private struct Bucket
{
/// <summary>
/// Inline entry, to avoid allocations for the common single entry case.
/// </summary>
public Entry InlineEntry;
/// <summary>
/// List of additional entries for the not-so-common multiple entries case.
/// </summary>
public List<Entry> MoreEntries;
}
private Bucket[] _buckets;
private int _count;
/// <summary>
/// Total amount of entries on the hash table.
/// </summary>
public int Count => _count;
/// <summary>
/// Creates a new instance of the partitioned hash table.
/// </summary>
public PartitionHashTable()
{
_buckets = Array.Empty<Bucket>();
}
/// <summary>
/// Gets an item on the table, or adds a new one if not present.
/// </summary>
/// <param name="data">Data</param>
/// <param name="dataHash">Hash of the data</param>
/// <param name="item">Item to be added if not found</param>
/// <returns>Existing item if found, or <paramref name="item"/> if not found</returns>
public T GetOrAdd(byte[] data, uint dataHash, T item)
{
if (TryFindItem(dataHash, data, out T existingItem))
{
return existingItem;
}
Entry entry = new Entry(dataHash, data, item);
AddToBucket(dataHash, ref entry);
return item;
}
/// <summary>
/// Adds an item to the hash table.
/// </summary>
/// <param name="data">Data</param>
/// <param name="dataHash">Hash of the data</param>
/// <param name="item">Item to be added</param>
/// <returns>True if the item was added, false due to an item associated with the data already being on the table</returns>
public bool Add(byte[] data, uint dataHash, T item)
{
if (TryFindItem(dataHash, data, out _))
{
return false;
}
Entry entry = new Entry(dataHash, data, item);
AddToBucket(dataHash, ref entry);
return true;
}
/// <summary>
/// Adds a partial entry to the hash table.
/// </summary>
/// <param name="ownerData">Full data</param>
/// <param name="ownSize">Size of the sub-region of <paramref name="ownerData"/> used by the partial entry</param>
/// <returns>True if added, false otherwise</returns>
public bool AddPartial(byte[] ownerData, int ownSize)
{
ReadOnlySpan<byte> data = new ReadOnlySpan<byte>(ownerData).Slice(0, ownSize);
return AddPartial(ownerData, HashState.CalcHash(data), ownSize);
}
/// <summary>
/// Adds a partial entry to the hash table.
/// </summary>
/// <param name="ownerData">Full data</param>
/// <param name="dataHash">Hash of the data sub-region</param>
/// <param name="ownSize">Size of the sub-region of <paramref name="ownerData"/> used by the partial entry</param>
/// <returns>True if added, false otherwise</returns>
public bool AddPartial(byte[] ownerData, uint dataHash, int ownSize)
{
ReadOnlySpan<byte> data = new ReadOnlySpan<byte>(ownerData).Slice(0, ownSize);
if (TryFindItem(dataHash, data, out _))
{
return false;
}
Entry entry = new Entry(dataHash, ownerData, ownSize);
AddToBucket(dataHash, ref entry);
return true;
}
/// <summary>
/// Adds entry with a given hash to the table.
/// </summary>
/// <param name="dataHash">Hash of the entry</param>
/// <param name="entry">Entry</param>
private void AddToBucket(uint dataHash, ref Entry entry)
{
int pow2Count = GetPow2Count(++_count);
if (pow2Count != _buckets.Length)
{
Rebuild(pow2Count);
}
ref Bucket bucket = ref GetBucketForHash(dataHash);
AddToBucket(ref bucket, ref entry);
}
/// <summary>
/// Adds an entry to a bucket.
/// </summary>
/// <param name="bucket">Bucket to add the entry into</param>
/// <param name="entry">Entry to be added</param>
private void AddToBucket(ref Bucket bucket, ref Entry entry)
{
if (bucket.InlineEntry.Data == null)
{
bucket.InlineEntry = entry;
}
else
{
(bucket.MoreEntries ??= new List<Entry>()).Add(entry);
}
}
/// <summary>
/// Creates partial entries on a new hash table for all existing full entries.
/// </summary>
/// <remarks>
/// This should be called every time a new hash table is created, and there are hash
/// tables with data sizes that are higher than that of the new table.
/// This will then fill the new hash table with "partial" entries of full entries
/// on the hash tables with higher size.
/// </remarks>
/// <param name="newTable">New hash table</param>
/// <param name="newEntrySize">Size of the data on the new hash table</param>
public void FillPartials(PartitionHashTable<T> newTable, int newEntrySize)
{
for (int i = 0; i < _buckets.Length; i++)
{
ref Bucket bucket = ref _buckets[i];
ref Entry inlineEntry = ref bucket.InlineEntry;
if (inlineEntry.Data != null)
{
if (!inlineEntry.IsPartial)
{
newTable.AddPartial(inlineEntry.Data, newEntrySize);
}
if (bucket.MoreEntries != null)
{
foreach (Entry entry in bucket.MoreEntries)
{
if (entry.IsPartial)
{
continue;
}
newTable.AddPartial(entry.Data, newEntrySize);
}
}
}
}
}
/// <summary>
/// Tries to find an item on the table.
/// </summary>
/// <param name="dataHash">Hash of <paramref name="data"/></param>
/// <param name="data">Data to find</param>
/// <param name="item">Item associated with the data</param>
/// <returns>True if an item was found, false otherwise</returns>
private bool TryFindItem(uint dataHash, ReadOnlySpan<byte> data, out T item)
{
if (_count == 0)
{
item = default;
return false;
}
ref Bucket bucket = ref GetBucketForHash(dataHash);
if (bucket.InlineEntry.Data != null)
{
if (bucket.InlineEntry.Hash == dataHash && bucket.InlineEntry.GetData().SequenceEqual(data))
{
item = bucket.InlineEntry.Item;
return true;
}
if (bucket.MoreEntries != null)
{
foreach (Entry entry in bucket.MoreEntries)
{
if (entry.Hash == dataHash && entry.GetData().SequenceEqual(data))
{
item = entry.Item;
return true;
}
}
}
}
item = default;
return false;
}
/// <summary>
/// Indicates the result of a hash table lookup.
/// </summary>
public enum SearchResult
{
/// <summary>
/// No entry was found, the search must continue on hash tables of lower size.
/// </summary>
NotFound,
/// <summary>
/// A partial entry was found, the search must continue on hash tables of higher size.
/// </summary>
FoundPartial,
/// <summary>
/// A full entry was found, the search was concluded and the item can be retrieved.
/// </summary>
FoundFull
}
/// <summary>
/// Tries to find an item on the table.
/// </summary>
/// <param name="dataAccessor">Data accessor</param>
/// <param name="size">Size of the hash table data</param>
/// <param name="item">The item on the table, if found, otherwise unmodified</param>
/// <param name="data">The data on the table, if found, otherwise unmodified</param>
/// <returns>Table lookup result</returns>
public SearchResult TryFindItem(ref SmartDataAccessor dataAccessor, int size, ref T item, ref byte[] data)
{
if (_count == 0)
{
return SearchResult.NotFound;
}
ReadOnlySpan<byte> dataSpan = dataAccessor.GetSpanAndHash(size, out uint dataHash);
if (dataSpan.Length != size)
{
return SearchResult.NotFound;
}
ref Bucket bucket = ref GetBucketForHash(dataHash);
if (bucket.InlineEntry.Data != null)
{
if (bucket.InlineEntry.Hash == dataHash && bucket.InlineEntry.GetData().SequenceEqual(dataSpan))
{
item = bucket.InlineEntry.Item;
data = bucket.InlineEntry.Data;
return bucket.InlineEntry.IsPartial ? SearchResult.FoundPartial : SearchResult.FoundFull;
}
if (bucket.MoreEntries != null)
{
foreach (Entry entry in bucket.MoreEntries)
{
if (entry.Hash == dataHash && entry.GetData().SequenceEqual(dataSpan))
{
item = entry.Item;
data = entry.Data;
return entry.IsPartial ? SearchResult.FoundPartial : SearchResult.FoundFull;
}
}
}
}
return SearchResult.NotFound;
}
/// <summary>
/// Rebuilds the table for a new count.
/// </summary>
/// <param name="newPow2Count">New power of two count of the table</param>
private void Rebuild(int newPow2Count)
{
Bucket[] newBuckets = new Bucket[newPow2Count];
uint mask = (uint)newPow2Count - 1;
for (int i = 0; i < _buckets.Length; i++)
{
ref Bucket bucket = ref _buckets[i];
if (bucket.InlineEntry.Data != null)
{
AddToBucket(ref newBuckets[(int)(bucket.InlineEntry.Hash & mask)], ref bucket.InlineEntry);
if (bucket.MoreEntries != null)
{
foreach (Entry entry in bucket.MoreEntries)
{
Entry entryCopy = entry;
AddToBucket(ref newBuckets[(int)(entry.Hash & mask)], ref entryCopy);
}
}
}
}
_buckets = newBuckets;
}
/// <summary>
/// Gets the bucket for a given hash.
/// </summary>
/// <param name="hash">Data hash</param>
/// <returns>Bucket for the hash</returns>
private ref Bucket GetBucketForHash(uint hash)
{
int index = (int)(hash & (_buckets.Length - 1));
return ref _buckets[index];
}
/// <summary>
/// Gets a power of two count from a regular count.
/// </summary>
/// <param name="count">Count</param>
/// <returns>Power of two count</returns>
private static int GetPow2Count(int count)
{
// This returns the nearest power of two that is lower than count.
// This was done to optimize memory usage rather than performance.
return 1 << BitOperations.Log2((uint)count);
}
}
}

View file

@ -0,0 +1,244 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Ryujinx.Graphics.Gpu.Shader.HashTable
{
/// <summary>
/// Partitioned hash table.
/// </summary>
/// <typeparam name="T"></typeparam>
public class PartitionedHashTable<T>
{
/// <summary>
/// Entry for a given data size.
/// </summary>
private struct SizeEntry
{
/// <summary>
/// Size for the data that will be stored on the hash table on this entry.
/// </summary>
public int Size { get; }
/// <summary>
/// Number of entries on the hash table.
/// </summary>
public int TableCount => _table.Count;
private readonly PartitionHashTable<T> _table;
/// <summary>
/// Creates an entry for a given size.
/// </summary>
/// <param name="size">Size of the data to be stored on this entry</param>
public SizeEntry(int size)
{
Size = size;
_table = new PartitionHashTable<T>();
}
/// <summary>
/// Gets an item for existing data, or adds a new one.
/// </summary>
/// <param name="data">Data associated with the item</param>
/// <param name="dataHash">Hash of <paramref name="data"/></param>
/// <param name="item">Item to be added</param>
/// <returns>Existing item, or <paramref name="item"/> if not present</returns>
public T GetOrAdd(byte[] data, uint dataHash, T item)
{
Debug.Assert(data.Length == Size);
return _table.GetOrAdd(data, dataHash, item);
}
/// <summary>
/// Adds a new item.
/// </summary>
/// <param name="data">Data associated with the item</param>
/// <param name="dataHash">Hash of <paramref name="data"/></param>
/// <param name="item">Item to be added</param>
/// <returns>True if added, false otherwise</returns>
public bool Add(byte[] data, uint dataHash, T item)
{
Debug.Assert(data.Length == Size);
return _table.Add(data, dataHash, item);
}
/// <summary>
/// Adds a partial entry.
/// </summary>
/// <param name="ownerData">Full entry data</param>
/// <param name="dataHash">Hash of the sub-region of the data that belongs to this entry</param>
/// <returns>True if added, false otherwise</returns>
public bool AddPartial(byte[] ownerData, uint dataHash)
{
return _table.AddPartial(ownerData, dataHash, Size);
}
/// <summary>
/// Fills a new hash table with "partials" of existing full entries of higher size.
/// </summary>
/// <param name="newEntry">Entry with the new hash table</param>
public void FillPartials(SizeEntry newEntry)
{
Debug.Assert(newEntry.Size < Size);
_table.FillPartials(newEntry._table, newEntry.Size);
}
/// <summary>
/// Tries to find an item on the hash table.
/// </summary>
/// <param name="dataAccessor">Data accessor</param>
/// <param name="item">The item on the table, if found, otherwise unmodified</param>
/// <param name="data">The data on the table, if found, otherwise unmodified</param>
/// <returns>Table lookup result</returns>
public PartitionHashTable<T>.SearchResult TryFindItem(ref SmartDataAccessor dataAccessor, ref T item, ref byte[] data)
{
return _table.TryFindItem(ref dataAccessor, Size, ref item, ref data);
}
}
private readonly List<SizeEntry> _sizeTable;
/// <summary>
/// Creates a new partitioned hash table.
/// </summary>
public PartitionedHashTable()
{
_sizeTable = new List<SizeEntry>();
}
/// <summary>
/// Adds a new item to the table.
/// </summary>
/// <param name="data">Data</param>
/// <param name="item">Item associated with the data</param>
public void Add(byte[] data, T item)
{
GetOrAdd(data, item);
}
/// <summary>
/// Gets an existing item from the table, or adds a new one if not present.
/// </summary>
/// <param name="data">Data</param>
/// <param name="item">Item associated with the data</param>
/// <returns>Existing item, or <paramref name="item"/> if not present</returns>
public T GetOrAdd(byte[] data, T item)
{
SizeEntry sizeEntry;
int index = BinarySearch(_sizeTable, data.Length);
if (index < _sizeTable.Count && _sizeTable[index].Size == data.Length)
{
sizeEntry = _sizeTable[index];
}
else
{
if (index < _sizeTable.Count && _sizeTable[index].Size < data.Length)
{
index++;
}
sizeEntry = new SizeEntry(data.Length);
_sizeTable.Insert(index, sizeEntry);
for (int i = index + 1; i < _sizeTable.Count; i++)
{
_sizeTable[i].FillPartials(sizeEntry);
}
}
HashState hashState = new HashState();
hashState.Initialize();
for (int i = 0; i < index; i++)
{
ReadOnlySpan<byte> dataSlice = new ReadOnlySpan<byte>(data).Slice(0, _sizeTable[i].Size);
hashState.Continue(dataSlice);
_sizeTable[i].AddPartial(data, hashState.Finalize(dataSlice));
}
hashState.Continue(data);
return sizeEntry.GetOrAdd(data, hashState.Finalize(data), item);
}
/// <summary>
/// Performs binary search on a list of hash tables, each one with a fixed data size.
/// </summary>
/// <param name="entries">List of hash tables</param>
/// <param name="size">Size to search for</param>
/// <returns>Index of the hash table with the given size, or nearest one otherwise</returns>
private static int BinarySearch(List<SizeEntry> entries, int size)
{
int left = 0;
int middle = 0;
int right = entries.Count - 1;
while (left <= right)
{
middle = left + ((right - left) >> 1);
SizeEntry entry = entries[middle];
if (size == entry.Size)
{
break;
}
if (size < entry.Size)
{
right = middle - 1;
}
else
{
left = middle + 1;
}
}
return middle;
}
/// <summary>
/// Tries to find an item on the table.
/// </summary>
/// <param name="dataAccessor">Data accessor</param>
/// <param name="item">Item, if found</param>
/// <param name="data">Data, if found</param>
/// <returns>True if the item was found on the table, false otherwise</returns>
public bool TryFindItem(IDataAccessor dataAccessor, out T item, out byte[] data)
{
SmartDataAccessor sda = new SmartDataAccessor(dataAccessor);
item = default;
data = null;
int left = 0;
int right = _sizeTable.Count;
while (left != right)
{
int index = left + ((right - left) >> 1);
PartitionHashTable<T>.SearchResult result = _sizeTable[index].TryFindItem(ref sda, ref item, ref data);
if (result == PartitionHashTable<T>.SearchResult.FoundFull)
{
return true;
}
if (result == PartitionHashTable<T>.SearchResult.NotFound)
{
right = index;
}
else /* if (result == PartitionHashTable<T>.SearchResult.FoundPartial) */
{
left = index + 1;
}
}
data = null;
return false;
}
}
}

View file

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Shader.HashTable
{
/// <summary>
/// Smart data accessor that can cache data and hashes to avoid reading and re-hashing the same memory regions.
/// </summary>
ref struct SmartDataAccessor
{
private readonly IDataAccessor _dataAccessor;
private ReadOnlySpan<byte> _data;
private readonly SortedList<int, HashState> _cachedHashes;
/// <summary>
/// Creates a new smart data accessor.
/// </summary>
/// <param name="dataAccessor">Data accessor</param>
public SmartDataAccessor(IDataAccessor dataAccessor)
{
_dataAccessor = dataAccessor;
_data = ReadOnlySpan<byte>.Empty;
_cachedHashes = new SortedList<int, HashState>();
}
/// <summary>
/// Get a spans of a given size.
/// </summary>
/// <remarks>
/// The actual length of the span returned depends on the <see cref="IDataAccessor"/>
/// and might be less than requested.
/// </remarks>
/// <param name="length">Size in bytes</param>
/// <returns>Span with the requested size</returns>
public ReadOnlySpan<byte> GetSpan(int length)
{
if (_data.Length < length)
{
_data = _dataAccessor.GetSpan(0, length);
}
else if (_data.Length > length)
{
return _data.Slice(0, length);
}
return _data;
}
/// <summary>
/// Gets a span of the requested size, and a hash of its data.
/// </summary>
/// <param name="length">Length of the span</param>
/// <param name="hash">Hash of the span data</param>
/// <returns>Span of data</returns>
public ReadOnlySpan<byte> GetSpanAndHash(int length, out uint hash)
{
ReadOnlySpan<byte> data = GetSpan(length);
hash = data.Length == length ? CalcHashCached(data) : 0;
return data;
}
/// <summary>
/// Calculates the hash for a requested span.
/// This will try to use a cached hash if the data was already accessed before, to avoid re-hashing.
/// </summary>
/// <param name="data">Data to be hashed</param>
/// <returns>Hash of the data</returns>
private uint CalcHashCached(ReadOnlySpan<byte> data)
{
HashState state = default;
bool found = false;
for (int i = _cachedHashes.Count - 1; i >= 0; i--)
{
int cachedHashSize = _cachedHashes.Keys[i];
if (cachedHashSize < data.Length)
{
state = _cachedHashes.Values[i];
found = true;
break;
}
}
if (!found)
{
state = new HashState();
state.Initialize();
}
state.Continue(data);
_cachedHashes[data.Length & ~7] = state;
return state.Finalize(data);
}
}
}

View file

@ -0,0 +1,36 @@
namespace Ryujinx.Graphics.Gpu.Shader
{
/// <summary>
/// Holds counts for the resources used by a shader.
/// </summary>
class ResourceCounts
{
/// <summary>
/// Total of uniform buffers used by the shaders.
/// </summary>
public int UniformBuffersCount;
/// <summary>
/// Total of storage buffers used by the shaders.
/// </summary>
public int StorageBuffersCount;
/// <summary>
/// Total of textures used by the shaders.
/// </summary>
public int TexturesCount;
/// <summary>
/// Total of images used by the shaders.
/// </summary>
public int ImagesCount;
/// <summary>
/// Creates a new instance of the shader resource counts class.
/// </summary>
public ResourceCounts()
{
UniformBuffersCount = 1; // The first binding is reserved for the support buffer.
}
}
}

View file

@ -1,4 +1,6 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Shader
{
@ -9,7 +11,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
{
#pragma warning disable CS0649
public ulong VertexA;
public ulong Vertex;
public ulong VertexB;
public ulong TessControl;
public ulong TessEvaluation;
public ulong Geometry;
@ -34,7 +36,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
public bool Equals(ShaderAddresses other)
{
return VertexA == other.VertexA &&
Vertex == other.Vertex &&
VertexB == other.VertexB &&
TessControl == other.TessControl &&
TessEvaluation == other.TessEvaluation &&
Geometry == other.Geometry &&
@ -47,7 +49,16 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// <returns>Hash code</returns>
public override int GetHashCode()
{
return HashCode.Combine(VertexA, Vertex, TessControl, TessEvaluation, Geometry, Fragment);
return HashCode.Combine(VertexA, VertexB, TessControl, TessEvaluation, Geometry, Fragment);
}
/// <summary>
/// Gets a view of the structure as a span of addresses.
/// </summary>
/// <returns>Span of addresses</returns>
public Span<ulong> AsSpan()
{
return MemoryMarshal.CreateSpan(ref VertexA, Unsafe.SizeOf<ShaderAddresses>() / sizeof(ulong));
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,280 @@
using Ryujinx.Graphics.Gpu.Memory;
using Ryujinx.Graphics.Gpu.Shader.HashTable;
using Ryujinx.Graphics.Shader;
using System;
using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Shader
{
/// <summary>
/// Holds already cached code for a guest shader.
/// </summary>
struct CachedGraphicsGuestCode
{
public byte[] VertexACode;
public byte[] VertexBCode;
public byte[] TessControlCode;
public byte[] TessEvaluationCode;
public byte[] GeometryCode;
public byte[] FragmentCode;
/// <summary>
/// Gets the guest code of a shader stage by its index.
/// </summary>
/// <param name="stageIndex">Index of the shader stage</param>
/// <returns>Guest code, or null if not present</returns>
public byte[] GetByIndex(int stageIndex)
{
return stageIndex switch
{
1 => TessControlCode,
2 => TessEvaluationCode,
3 => GeometryCode,
4 => FragmentCode,
_ => VertexBCode
};
}
}
/// <summary>
/// Graphics shader cache hash table.
/// </summary>
class ShaderCacheHashTable
{
/// <summary>
/// Shader ID cache.
/// </summary>
private struct IdCache
{
private PartitionedHashTable<int> _cache;
private int _id;
/// <summary>
/// Initializes the state.
/// </summary>
public void Initialize()
{
_cache = new PartitionedHashTable<int>();
_id = 0;
}
/// <summary>
/// Adds guest code to the cache.
/// </summary>
/// <remarks>
/// If the code was already cached, it will just return the existing ID.
/// </remarks>
/// <param name="code">Code to add</param>
/// <returns>Unique ID for the guest code</returns>
public int Add(byte[] code)
{
int id = ++_id;
int cachedId = _cache.GetOrAdd(code, id);
if (cachedId != id)
{
--_id;
}
return cachedId;
}
/// <summary>
/// Tries to find cached guest code.
/// </summary>
/// <param name="dataAccessor">Code accessor used to read guest code to find a match on the hash table</param>
/// <param name="id">ID of the guest code, if found</param>
/// <param name="data">Cached guest code, if found</param>
/// <returns>True if found, false otherwise</returns>
public bool TryFind(IDataAccessor dataAccessor, out int id, out byte[] data)
{
return _cache.TryFindItem(dataAccessor, out id, out data);
}
}
/// <summary>
/// Guest code IDs of the guest shaders that when combined forms a single host program.
/// </summary>
private struct IdTable : IEquatable<IdTable>
{
public int VertexAId;
public int VertexBId;
public int TessControlId;
public int TessEvaluationId;
public int GeometryId;
public int FragmentId;
public override bool Equals(object obj)
{
return obj is IdTable other && Equals(other);
}
public bool Equals(IdTable other)
{
return other.VertexAId == VertexAId &&
other.VertexBId == VertexBId &&
other.TessControlId == TessControlId &&
other.TessEvaluationId == TessEvaluationId &&
other.GeometryId == GeometryId &&
other.FragmentId == FragmentId;
}
public override int GetHashCode()
{
return HashCode.Combine(VertexAId, VertexBId, TessControlId, TessEvaluationId, GeometryId, FragmentId);
}
}
private IdCache _vertexACache;
private IdCache _vertexBCache;
private IdCache _tessControlCache;
private IdCache _tessEvaluationCache;
private IdCache _geometryCache;
private IdCache _fragmentCache;
private readonly Dictionary<IdTable, ShaderSpecializationList> _shaderPrograms;
/// <summary>
/// Creates a new graphics shader cache hash table.
/// </summary>
public ShaderCacheHashTable()
{
_vertexACache.Initialize();
_vertexBCache.Initialize();
_tessControlCache.Initialize();
_tessEvaluationCache.Initialize();
_geometryCache.Initialize();
_fragmentCache.Initialize();
_shaderPrograms = new Dictionary<IdTable, ShaderSpecializationList>();
}
/// <summary>
/// Adds a program to the cache.
/// </summary>
/// <param name="program">Program to be added</param>
public void Add(CachedShaderProgram program)
{
IdTable idTable = new IdTable();
foreach (var shader in program.Shaders)
{
if (shader == null)
{
continue;
}
if (shader.Info != null)
{
switch (shader.Info.Stage)
{
case ShaderStage.Vertex:
idTable.VertexBId = _vertexBCache.Add(shader.Code);
break;
case ShaderStage.TessellationControl:
idTable.TessControlId = _tessControlCache.Add(shader.Code);
break;
case ShaderStage.TessellationEvaluation:
idTable.TessEvaluationId = _tessEvaluationCache.Add(shader.Code);
break;
case ShaderStage.Geometry:
idTable.GeometryId = _geometryCache.Add(shader.Code);
break;
case ShaderStage.Fragment:
idTable.FragmentId = _fragmentCache.Add(shader.Code);
break;
}
}
else
{
idTable.VertexAId = _vertexACache.Add(shader.Code);
}
}
if (!_shaderPrograms.TryGetValue(idTable, out ShaderSpecializationList specList))
{
specList = new ShaderSpecializationList();
_shaderPrograms.Add(idTable, specList);
}
specList.Add(program);
}
/// <summary>
/// Tries to find a cached program.
/// </summary>
/// <remarks>
/// Even if false is returned, <paramref name="guestCode"/> might still contain cached guest code.
/// This can be used to avoid additional allocations for guest code that was already cached.
/// </remarks>
/// <param name="channel">GPU channel</param>
/// <param name="poolState">Texture pool state</param>
/// <param name="addresses">Guest addresses of the shaders to find</param>
/// <param name="program">Cached host program for the given state, if found</param>
/// <param name="guestCode">Cached guest code, if any found</param>
/// <returns>True if a cached host program was found, false otherwise</returns>
public bool TryFind(
GpuChannel channel,
GpuChannelPoolState poolState,
ShaderAddresses addresses,
out CachedShaderProgram program,
out CachedGraphicsGuestCode guestCode)
{
var memoryManager = channel.MemoryManager;
IdTable idTable = new IdTable();
guestCode = new CachedGraphicsGuestCode();
program = null;
bool found = TryGetId(_vertexACache, memoryManager, addresses.VertexA, out idTable.VertexAId, out guestCode.VertexACode);
found &= TryGetId(_vertexBCache, memoryManager, addresses.VertexB, out idTable.VertexBId, out guestCode.VertexBCode);
found &= TryGetId(_tessControlCache, memoryManager, addresses.TessControl, out idTable.TessControlId, out guestCode.TessControlCode);
found &= TryGetId(_tessEvaluationCache, memoryManager, addresses.TessEvaluation, out idTable.TessEvaluationId, out guestCode.TessEvaluationCode);
found &= TryGetId(_geometryCache, memoryManager, addresses.Geometry, out idTable.GeometryId, out guestCode.GeometryCode);
found &= TryGetId(_fragmentCache, memoryManager, addresses.Fragment, out idTable.FragmentId, out guestCode.FragmentCode);
if (found && _shaderPrograms.TryGetValue(idTable, out ShaderSpecializationList specList))
{
return specList.TryFindForGraphics(channel, poolState, out program);
}
return false;
}
/// <summary>
/// Tries to get the ID of a single cached shader stage.
/// </summary>
/// <param name="idCache">ID cache of the stage</param>
/// <param name="memoryManager">GPU memory manager</param>
/// <param name="baseAddress">Base address of the shader</param>
/// <param name="id">ID, if found</param>
/// <param name="data">Cached guest code, if found</param>
/// <returns>True if a cached shader is found, false otherwise</returns>
private static bool TryGetId(IdCache idCache, MemoryManager memoryManager, ulong baseAddress, out int id, out byte[] data)
{
if (baseAddress == 0)
{
id = 0;
data = null;
return true;
}
ShaderCodeAccessor codeAccessor = new ShaderCodeAccessor(memoryManager, baseAddress);
return idCache.TryFind(codeAccessor, out id, out data);
}
/// <summary>
/// Gets all programs that have been added to the table.
/// </summary>
/// <returns>Programs added to the table</returns>
public IEnumerable<CachedShaderProgram> GetPrograms()
{
foreach (var specList in _shaderPrograms.Values)
{
foreach (var program in specList)
{
yield return program;
}
}
}
}
}

View file

@ -0,0 +1,32 @@
using Ryujinx.Graphics.Gpu.Memory;
using Ryujinx.Graphics.Gpu.Shader.HashTable;
using System;
namespace Ryujinx.Graphics.Gpu.Shader
{
/// <summary>
/// Shader code accessor.
/// </summary>
struct ShaderCodeAccessor : IDataAccessor
{
private readonly MemoryManager _memoryManager;
private readonly ulong _baseAddress;
/// <summary>
/// Creates a new shader code accessor.
/// </summary>
/// <param name="memoryManager">Memory manager used to access the shader code</param>
/// <param name="baseAddress">Base address of the shader in memory</param>
public ShaderCodeAccessor(MemoryManager memoryManager, ulong baseAddress)
{
_memoryManager = memoryManager;
_baseAddress = baseAddress;
}
/// <inheritdoc/>
public ReadOnlySpan<byte> GetSpan(int offset, int length)
{
return _memoryManager.GetSpanMapped(_baseAddress + (ulong)offset, length);
}
}
}

View file

@ -1,52 +0,0 @@
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader;
namespace Ryujinx.Graphics.Gpu.Shader
{
/// <summary>
/// Cached shader code for a single shader stage.
/// </summary>
class ShaderCodeHolder
{
/// <summary>
/// Shader program containing translated code.
/// </summary>
public ShaderProgram Program { get; }
/// <summary>
/// Shader program information.
/// </summary>
public ShaderProgramInfo Info { get; }
/// <summary>
/// Host shader object.
/// </summary>
/// <remarks>Null if the host shader program cache is in use.</remarks>
public IShader HostShader { get; set; }
/// <summary>
/// Maxwell binary shader code.
/// </summary>
public byte[] Code { get; }
/// <summary>
/// Optional maxwell binary shader code for "Vertex A" shader.
/// </summary>
public byte[] Code2 { get; }
/// <summary>
/// Creates a new instace of the shader code holder.
/// </summary>
/// <param name="program">Shader program</param>
/// <param name="info">Shader program information</param>
/// <param name="code">Maxwell binary shader code</param>
/// <param name="code2">Optional binary shader code of the "Vertex A" shader, when combined with "Vertex B"</param>
public ShaderCodeHolder(ShaderProgram program, ShaderProgramInfo info, byte[] code, byte[] code2 = null)
{
Program = program;
Info = info;
Code = code;
Code2 = code2;
}
}
}

View file

@ -1,95 +0,0 @@
using Ryujinx.Graphics.GAL;
using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.Graphics.Gpu.Shader
{
delegate bool ShaderCompileTaskCallback(bool success, ShaderCompileTask task);
/// <summary>
/// A class that represents a shader compilation.
/// </summary>
class ShaderCompileTask
{
private bool _compiling;
private Task _programsTask;
private IProgram _program;
private ShaderCompileTaskCallback _action;
private AutoResetEvent _taskDoneEvent;
public bool IsFaulted => _programsTask.IsFaulted;
/// <summary>
/// Create a new shader compile task, with an event to signal whenever a subtask completes.
/// </summary>
/// <param name="taskDoneEvent">Event to signal when a subtask completes</param>
public ShaderCompileTask(AutoResetEvent taskDoneEvent)
{
_taskDoneEvent = taskDoneEvent;
}
/// <summary>
/// Check the completion status of the shader compile task, and run callbacks on step completion.
/// Calling this periodically is required to progress through steps of the compilation.
/// </summary>
/// <returns>True if the task is complete, false if it is in progress</returns>
public bool IsDone()
{
if (_compiling)
{
ProgramLinkStatus status = _program.CheckProgramLink(false);
if (status != ProgramLinkStatus.Incomplete)
{
return _action(status == ProgramLinkStatus.Success, this);
}
}
else
{
// Waiting on the task.
if (_programsTask.IsCompleted)
{
return _action(true, this);
}
}
return false;
}
/// <summary>
/// Run a callback when the specified task has completed.
/// </summary>
/// <param name="task">The task object that needs to complete</param>
/// <param name="action">The action to perform when it is complete</param>
public void OnTask(Task task, ShaderCompileTaskCallback action)
{
_compiling = false;
_programsTask = task;
_action = action;
task.ContinueWith(task => _taskDoneEvent.Set());
}
/// <summary>
/// Run a callback when the specified program has been linked.
/// </summary>
/// <param name="task">The program that needs to be linked</param>
/// <param name="action">The action to perform when linking is complete</param>
public void OnCompiled(IProgram program, ShaderCompileTaskCallback action)
{
_compiling = true;
_program = program;
_action = action;
if (program == null)
{
action(false, this);
}
}
}
}

View file

@ -0,0 +1,76 @@
using System.Collections;
using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Shader
{
/// <summary>
/// List of cached shader programs that differs only by specialization state.
/// </summary>
class ShaderSpecializationList : IEnumerable<CachedShaderProgram>
{
private readonly List<CachedShaderProgram> _entries = new List<CachedShaderProgram>();
/// <summary>
/// Adds a program to the list.
/// </summary>
/// <param name="program">Program to be added</param>
public void Add(CachedShaderProgram program)
{
_entries.Add(program);
}
/// <summary>
/// Tries to find an existing 3D program on the cache.
/// </summary>
/// <param name="channel">GPU channel</param>
/// <param name="poolState">Texture pool state</param>
/// <param name="program">Cached program, if found</param>
/// <returns>True if a compatible program is found, false otherwise</returns>
public bool TryFindForGraphics(GpuChannel channel, GpuChannelPoolState poolState, out CachedShaderProgram program)
{
foreach (var entry in _entries)
{
if (entry.SpecializationState.MatchesGraphics(channel, poolState))
{
program = entry;
return true;
}
}
program = default;
return false;
}
/// <summary>
/// Tries to find an existing compute program on the cache.
/// </summary>
/// <param name="channel">GPU channel</param>
/// <param name="poolState">Texture pool state</param>
/// <param name="program">Cached program, if found</param>
/// <returns>True if a compatible program is found, false otherwise</returns>
public bool TryFindForCompute(GpuChannel channel, GpuChannelPoolState poolState, out CachedShaderProgram program)
{
foreach (var entry in _entries)
{
if (entry.SpecializationState.MatchesCompute(channel, poolState))
{
program = entry;
return true;
}
}
program = default;
return false;
}
public IEnumerator<CachedShaderProgram> GetEnumerator()
{
return _entries.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View file

@ -0,0 +1,615 @@
using Ryujinx.Common.Memory;
using Ryujinx.Graphics.Gpu.Shader.DiskCache;
using Ryujinx.Graphics.Shader;
using System;
using System.Collections.Generic;
using System.Numerics;
namespace Ryujinx.Graphics.Gpu.Shader
{
class ShaderSpecializationState
{
private const uint ComsMagic = (byte)'C' | ((byte)'O' << 8) | ((byte)'M' << 16) | ((byte)'S' << 24);
private const uint GfxsMagic = (byte)'G' | ((byte)'F' << 8) | ((byte)'X' << 16) | ((byte)'S' << 24);
private const uint TfbdMagic = (byte)'T' | ((byte)'F' << 8) | ((byte)'B' << 16) | ((byte)'D' << 24);
private const uint TexkMagic = (byte)'T' | ((byte)'E' << 8) | ((byte)'X' << 16) | ((byte)'K' << 24);
private const uint TexsMagic = (byte)'T' | ((byte)'E' << 8) | ((byte)'X' << 16) | ((byte)'S' << 24);
/// <summary>
/// Flags indicating GPU state that is used by the shader.
/// </summary>
[Flags]
private enum QueriedStateFlags
{
EarlyZForce = 1 << 0,
PrimitiveTopology = 1 << 1,
TessellationMode = 1 << 2,
TransformFeedback = 1 << 3
}
private QueriedStateFlags _queriedState;
private bool _compute;
private byte _constantBufferUsePerStage;
/// <summary>
/// Compute engine state.
/// </summary>
public GpuChannelComputeState ComputeState;
/// <summary>
/// 3D engine state.
/// </summary>
public GpuChannelGraphicsState GraphicsState;
/// <summary>
/// Contant buffers bound at the time the shader was compiled, per stage.
/// </summary>
public Array5<uint> ConstantBufferUse;
/// <summary>
/// Transform feedback buffers active at the time the shader was compiled.
/// </summary>
public TransformFeedbackDescriptor[] TransformFeedbackDescriptors;
/// <summary>
/// Flags indicating texture state that is used by the shader.
/// </summary>
[Flags]
private enum QueriedTextureStateFlags
{
TextureFormat = 1 << 0,
SamplerType = 1 << 1,
CoordNormalized = 1 << 2
}
/// <summary>
/// Reference type wrapping a value.
/// </summary>
private class Box<T>
{
/// <summary>
/// Wrapped value.
/// </summary>
public T Value;
}
/// <summary>
/// State of a texture or image that is accessed by the shader.
/// </summary>
private struct TextureSpecializationState
{
// New fields should be added to the end of the struct to keep disk shader cache compatibility.
/// <summary>
/// Flags indicating which state of the texture the shader depends on.
/// </summary>
public QueriedTextureStateFlags QueriedFlags;
/// <summary>
/// Encoded texture format value.
/// </summary>
public uint Format;
/// <summary>
/// True if the texture format is sRGB, false otherwise.
/// </summary>
public bool FormatSrgb;
/// <summary>
/// Texture target.
/// </summary>
public Image.TextureTarget TextureTarget;
/// <summary>
/// Indicates if the coordinates used to sample the texture are normalized or not (0.0..1.0 or 0..Width/Height).
/// </summary>
public bool CoordNormalized;
}
/// <summary>
/// Texture binding information, used to identify each texture accessed by the shader.
/// </summary>
private struct TextureKey : IEquatable<TextureKey>
{
// New fields should be added to the end of the struct to keep disk shader cache compatibility.
/// <summary>
/// Shader stage where the texture is used.
/// </summary>
public readonly int StageIndex;
/// <summary>
/// Texture handle offset in words on the texture buffer.
/// </summary>
public readonly int Handle;
/// <summary>
/// Constant buffer slot of the texture buffer (-1 to use the texture buffer index GPU register).
/// </summary>
public readonly int CbufSlot;
/// <summary>
/// Creates a new texture key.
/// </summary>
/// <param name="stageIndex">Shader stage where the texture is used</param>
/// <param name="handle">Texture handle offset in words on the texture buffer</param>
/// <param name="cbufSlot">Constant buffer slot of the texture buffer (-1 to use the texture buffer index GPU register)</param>
public TextureKey(int stageIndex, int handle, int cbufSlot)
{
StageIndex = stageIndex;
Handle = handle;
CbufSlot = cbufSlot;
}
public override bool Equals(object obj)
{
return obj is TextureKey textureKey && Equals(textureKey);
}
public bool Equals(TextureKey other)
{
return StageIndex == other.StageIndex && Handle == other.Handle && CbufSlot == other.CbufSlot;
}
public override int GetHashCode()
{
return HashCode.Combine(StageIndex, Handle, CbufSlot);
}
}
private readonly Dictionary<TextureKey, Box<TextureSpecializationState>> _textureSpecialization;
/// <summary>
/// Creates a new instance of the shader specialization state.
/// </summary>
private ShaderSpecializationState()
{
_textureSpecialization = new Dictionary<TextureKey, Box<TextureSpecializationState>>();
}
/// <summary>
/// Creates a new instance of the shader specialization state.
/// </summary>
/// <param name="state">Current compute engine state</param>
public ShaderSpecializationState(GpuChannelComputeState state) : this()
{
ComputeState = state;
_compute = true;
}
/// <summary>
/// Creates a new instance of the shader specialization state.
/// </summary>
/// <param name="state">Current 3D engine state</param>
/// <param name="descriptors">Optional transform feedback buffers in use, if any</param>
public ShaderSpecializationState(GpuChannelGraphicsState state, TransformFeedbackDescriptor[] descriptors) : this()
{
GraphicsState = state;
_compute = false;
if (descriptors != null)
{
TransformFeedbackDescriptors = descriptors;
_queriedState |= QueriedStateFlags.TransformFeedback;
}
}
/// <summary>
/// Indicates that the shader accesses the early Z force state.
/// </summary>
public void RecordEarlyZForce()
{
_queriedState |= QueriedStateFlags.EarlyZForce;
}
/// <summary>
/// Indicates that the shader accesses the primitive topology state.
/// </summary>
public void RecordPrimitiveTopology()
{
_queriedState |= QueriedStateFlags.PrimitiveTopology;
}
/// <summary>
/// Indicates that the shader accesses the tessellation mode state.
/// </summary>
public void RecordTessellationMode()
{
_queriedState |= QueriedStateFlags.TessellationMode;
}
/// <summary>
/// Indicates that the shader accesses the constant buffer use state.
/// </summary>
/// <param name="stageIndex">Shader stage index</param>
/// <param name="useMask">Mask indicating the constant buffers bound at the time of the shader compilation</param>
public void RecordConstantBufferUse(int stageIndex, uint useMask)
{
ConstantBufferUse[stageIndex] = useMask;
_constantBufferUsePerStage |= (byte)(1 << stageIndex);
}
/// <summary>
/// Indicates that a given texture is accessed by the shader.
/// </summary>
/// <param name="stageIndex">Shader stage where the texture is used</param>
/// <param name="handle">Offset in words of the texture handle on the texture buffer</param>
/// <param name="cbufSlot">Slot of the texture buffer constant buffer</param>
/// <param name="descriptor">Descriptor of the texture</param>
public void RegisterTexture(int stageIndex, int handle, int cbufSlot, Image.TextureDescriptor descriptor)
{
Box<TextureSpecializationState> state = GetOrCreateTextureSpecState(stageIndex, handle, cbufSlot);
state.Value.Format = descriptor.UnpackFormat();
state.Value.FormatSrgb = descriptor.UnpackSrgb();
state.Value.TextureTarget = descriptor.UnpackTextureTarget();
state.Value.CoordNormalized = descriptor.UnpackTextureCoordNormalized();
}
/// <summary>
/// Indicates that a given texture is accessed by the shader.
/// </summary>
/// <param name="stageIndex">Shader stage where the texture is used</param>
/// <param name="handle">Offset in words of the texture handle on the texture buffer</param>
/// <param name="cbufSlot">Slot of the texture buffer constant buffer</param>
/// <param name="format">Maxwell texture format value</param>
/// <param name="formatSrgb">Whenever the texture format is a sRGB format</param>
/// <param name="target">Texture target type</param>
/// <param name="coordNormalized">Whenever the texture coordinates used on the shader are considered normalized</param>
public void RegisterTexture(
int stageIndex,
int handle,
int cbufSlot,
uint format,
bool formatSrgb,
Image.TextureTarget target,
bool coordNormalized)
{
Box<TextureSpecializationState> state = GetOrCreateTextureSpecState(stageIndex, handle, cbufSlot);
state.Value.Format = format;
state.Value.FormatSrgb = formatSrgb;
state.Value.TextureTarget = target;
state.Value.CoordNormalized = coordNormalized;
}
/// <summary>
/// Indicates that the format of a given texture was used during the shader translation process.
/// </summary>
/// <param name="stageIndex">Shader stage where the texture is used</param>
/// <param name="handle">Offset in words of the texture handle on the texture buffer</param>
/// <param name="cbufSlot">Slot of the texture buffer constant buffer</param>
public void RecordTextureFormat(int stageIndex, int handle, int cbufSlot)
{
Box<TextureSpecializationState> state = GetOrCreateTextureSpecState(stageIndex, handle, cbufSlot);
state.Value.QueriedFlags |= QueriedTextureStateFlags.TextureFormat;
}
/// <summary>
/// Indicates that the target of a given texture was used during the shader translation process.
/// </summary>
/// <param name="stageIndex">Shader stage where the texture is used</param>
/// <param name="handle">Offset in words of the texture handle on the texture buffer</param>
/// <param name="cbufSlot">Slot of the texture buffer constant buffer</param>
public void RecordTextureSamplerType(int stageIndex, int handle, int cbufSlot)
{
Box<TextureSpecializationState> state = GetOrCreateTextureSpecState(stageIndex, handle, cbufSlot);
state.Value.QueriedFlags |= QueriedTextureStateFlags.SamplerType;
}
/// <summary>
/// Indicates that the coordinate normalization state of a given texture was used during the shader translation process.
/// </summary>
/// <param name="stageIndex">Shader stage where the texture is used</param>
/// <param name="handle">Offset in words of the texture handle on the texture buffer</param>
/// <param name="cbufSlot">Slot of the texture buffer constant buffer</param>
public void RecordTextureCoordNormalized(int stageIndex, int handle, int cbufSlot)
{
Box<TextureSpecializationState> state = GetOrCreateTextureSpecState(stageIndex, handle, cbufSlot);
state.Value.QueriedFlags |= QueriedTextureStateFlags.CoordNormalized;
}
/// <summary>
/// Checks if a given texture was registerd on this specialization state.
/// </summary>
/// <param name="stageIndex">Shader stage where the texture is used</param>
/// <param name="handle">Offset in words of the texture handle on the texture buffer</param>
/// <param name="cbufSlot">Slot of the texture buffer constant buffer</param>
public bool TextureRegistered(int stageIndex, int handle, int cbufSlot)
{
return GetTextureSpecState(stageIndex, handle, cbufSlot) != null;
}
/// <summary>
/// Gets the recorded format of a given texture.
/// </summary>
/// <param name="stageIndex">Shader stage where the texture is used</param>
/// <param name="handle">Offset in words of the texture handle on the texture buffer</param>
/// <param name="cbufSlot">Slot of the texture buffer constant buffer</param>
public (uint, bool) GetFormat(int stageIndex, int handle, int cbufSlot)
{
TextureSpecializationState state = GetTextureSpecState(stageIndex, handle, cbufSlot).Value;
return (state.Format, state.FormatSrgb);
}
/// <summary>
/// Gets the recorded target of a given texture.
/// </summary>
/// <param name="stageIndex">Shader stage where the texture is used</param>
/// <param name="handle">Offset in words of the texture handle on the texture buffer</param>
/// <param name="cbufSlot">Slot of the texture buffer constant buffer</param>
public Image.TextureTarget GetTextureTarget(int stageIndex, int handle, int cbufSlot)
{
return GetTextureSpecState(stageIndex, handle, cbufSlot).Value.TextureTarget;
}
/// <summary>
/// Gets the recorded coordinate normalization state of a given texture.
/// </summary>
/// <param name="stageIndex">Shader stage where the texture is used</param>
/// <param name="handle">Offset in words of the texture handle on the texture buffer</param>
/// <param name="cbufSlot">Slot of the texture buffer constant buffer</param>
public bool GetCoordNormalized(int stageIndex, int handle, int cbufSlot)
{
return GetTextureSpecState(stageIndex, handle, cbufSlot).Value.CoordNormalized;
}
/// <summary>
/// Gets texture specialization state for a given texture, or create a new one if not present.
/// </summary>
/// <param name="stageIndex">Shader stage where the texture is used</param>
/// <param name="handle">Offset in words of the texture handle on the texture buffer</param>
/// <param name="cbufSlot">Slot of the texture buffer constant buffer</param>
/// <returns>Texture specialization state</returns>
private Box<TextureSpecializationState> GetOrCreateTextureSpecState(int stageIndex, int handle, int cbufSlot)
{
TextureKey key = new TextureKey(stageIndex, handle, cbufSlot);
if (!_textureSpecialization.TryGetValue(key, out Box<TextureSpecializationState> state))
{
_textureSpecialization.Add(key, state = new Box<TextureSpecializationState>());
}
return state;
}
/// <summary>
/// Gets texture specialization state for a given texture.
/// </summary>
/// <param name="stageIndex">Shader stage where the texture is used</param>
/// <param name="handle">Offset in words of the texture handle on the texture buffer</param>
/// <param name="cbufSlot">Slot of the texture buffer constant buffer</param>
/// <returns>Texture specialization state</returns>
private Box<TextureSpecializationState> GetTextureSpecState(int stageIndex, int handle, int cbufSlot)
{
TextureKey key = new TextureKey(stageIndex, handle, cbufSlot);
if (_textureSpecialization.TryGetValue(key, out Box<TextureSpecializationState> state))
{
return state;
}
return null;
}
/// <summary>
/// Checks if the recorded state matches the current GPU 3D engine state.
/// </summary>
/// <param name="channel">GPU channel</param>
/// <param name="poolState">Texture pool state</param>
/// <returns>True if the state matches, false otherwise</returns>
public bool MatchesGraphics(GpuChannel channel, GpuChannelPoolState poolState)
{
return Matches(channel, poolState, isCompute: false);
}
/// <summary>
/// Checks if the recorded state matches the current GPU compute engine state.
/// </summary>
/// <param name="channel">GPU channel</param>
/// <param name="poolState">Texture pool state</param>
/// <returns>True if the state matches, false otherwise</returns>
public bool MatchesCompute(GpuChannel channel, GpuChannelPoolState poolState)
{
return Matches(channel, poolState, isCompute: true);
}
/// <summary>
/// Checks if the recorded state matches the current GPU state.
/// </summary>
/// <param name="channel">GPU channel</param>
/// <param name="poolState">Texture pool state</param>
/// <param name="isCompute">Indicates whenever the check is requested by the 3D or compute engine</param>
/// <returns>True if the state matches, false otherwise</returns>
private bool Matches(GpuChannel channel, GpuChannelPoolState poolState, bool isCompute)
{
int constantBufferUsePerStageMask = _constantBufferUsePerStage;
while (constantBufferUsePerStageMask != 0)
{
int index = BitOperations.TrailingZeroCount(constantBufferUsePerStageMask);
uint useMask = isCompute
? channel.BufferManager.GetComputeUniformBufferUseMask()
: channel.BufferManager.GetGraphicsUniformBufferUseMask(index);
if (ConstantBufferUse[index] != useMask)
{
return false;
}
constantBufferUsePerStageMask &= ~(1 << index);
}
foreach (var kv in _textureSpecialization)
{
TextureKey textureKey = kv.Key;
(int textureBufferIndex, int samplerBufferIndex) = TextureHandle.UnpackSlots(textureKey.CbufSlot, poolState.TextureBufferIndex);
ulong textureCbAddress;
ulong samplerCbAddress;
if (isCompute)
{
textureCbAddress = channel.BufferManager.GetComputeUniformBufferAddress(textureBufferIndex);
samplerCbAddress = channel.BufferManager.GetComputeUniformBufferAddress(samplerBufferIndex);
}
else
{
textureCbAddress = channel.BufferManager.GetGraphicsUniformBufferAddress(textureKey.StageIndex, textureBufferIndex);
samplerCbAddress = channel.BufferManager.GetGraphicsUniformBufferAddress(textureKey.StageIndex, samplerBufferIndex);
}
if (!channel.MemoryManager.Physical.IsMapped(textureCbAddress) || !channel.MemoryManager.Physical.IsMapped(samplerCbAddress))
{
continue;
}
Image.TextureDescriptor descriptor;
if (isCompute)
{
descriptor = channel.TextureManager.GetComputeTextureDescriptor(
poolState.TexturePoolGpuVa,
poolState.TextureBufferIndex,
poolState.TexturePoolMaximumId,
textureKey.Handle,
textureKey.CbufSlot);
}
else
{
descriptor = channel.TextureManager.GetGraphicsTextureDescriptor(
poolState.TexturePoolGpuVa,
poolState.TextureBufferIndex,
poolState.TexturePoolMaximumId,
textureKey.StageIndex,
textureKey.Handle,
textureKey.CbufSlot);
}
Box<TextureSpecializationState> specializationState = kv.Value;
if (specializationState.Value.QueriedFlags.HasFlag(QueriedTextureStateFlags.CoordNormalized) &&
specializationState.Value.CoordNormalized != descriptor.UnpackTextureCoordNormalized())
{
return false;
}
}
return true;
}
/// <summary>
/// Reads shader specialization state that has been serialized.
/// </summary>
/// <param name="dataReader">Data reader</param>
/// <returns>Shader specialization state</returns>
public static ShaderSpecializationState Read(ref BinarySerializer dataReader)
{
ShaderSpecializationState specState = new ShaderSpecializationState();
dataReader.Read(ref specState._queriedState);
dataReader.Read(ref specState._compute);
if (specState._compute)
{
dataReader.ReadWithMagicAndSize(ref specState.ComputeState, ComsMagic);
}
else
{
dataReader.ReadWithMagicAndSize(ref specState.GraphicsState, GfxsMagic);
}
dataReader.Read(ref specState._constantBufferUsePerStage);
int constantBufferUsePerStageMask = specState._constantBufferUsePerStage;
while (constantBufferUsePerStageMask != 0)
{
int index = BitOperations.TrailingZeroCount(constantBufferUsePerStageMask);
dataReader.Read(ref specState.ConstantBufferUse[index]);
constantBufferUsePerStageMask &= ~(1 << index);
}
if (specState._queriedState.HasFlag(QueriedStateFlags.TransformFeedback))
{
ushort tfCount = 0;
dataReader.Read(ref tfCount);
specState.TransformFeedbackDescriptors = new TransformFeedbackDescriptor[tfCount];
for (int index = 0; index < tfCount; index++)
{
dataReader.ReadWithMagicAndSize(ref specState.TransformFeedbackDescriptors[index], TfbdMagic);
}
}
ushort count = 0;
dataReader.Read(ref count);
for (int index = 0; index < count; index++)
{
TextureKey textureKey = default;
Box<TextureSpecializationState> textureState = new Box<TextureSpecializationState>();
dataReader.ReadWithMagicAndSize(ref textureKey, TexkMagic);
dataReader.ReadWithMagicAndSize(ref textureState.Value, TexsMagic);
specState._textureSpecialization[textureKey] = textureState;
}
return specState;
}
/// <summary>
/// Serializes the shader specialization state.
/// </summary>
/// <param name="dataWriter">Data writer</param>
public void Write(ref BinarySerializer dataWriter)
{
dataWriter.Write(ref _queriedState);
dataWriter.Write(ref _compute);
if (_compute)
{
dataWriter.WriteWithMagicAndSize(ref ComputeState, ComsMagic);
}
else
{
dataWriter.WriteWithMagicAndSize(ref GraphicsState, GfxsMagic);
}
dataWriter.Write(ref _constantBufferUsePerStage);
int constantBufferUsePerStageMask = _constantBufferUsePerStage;
while (constantBufferUsePerStageMask != 0)
{
int index = BitOperations.TrailingZeroCount(constantBufferUsePerStageMask);
dataWriter.Write(ref ConstantBufferUse[index]);
constantBufferUsePerStageMask &= ~(1 << index);
}
if (_queriedState.HasFlag(QueriedStateFlags.TransformFeedback))
{
ushort tfCount = (ushort)TransformFeedbackDescriptors.Length;
dataWriter.Write(ref tfCount);
for (int index = 0; index < TransformFeedbackDescriptors.Length; index++)
{
dataWriter.WriteWithMagicAndSize(ref TransformFeedbackDescriptors[index], TfbdMagic);
}
}
ushort count = (ushort)_textureSpecialization.Count;
dataWriter.Write(ref count);
foreach (var kv in _textureSpecialization)
{
var textureKey = kv.Key;
var textureState = kv.Value;
dataWriter.WriteWithMagicAndSize(ref textureKey, TexkMagic);
dataWriter.WriteWithMagicAndSize(ref textureState.Value, TexsMagic);
}
}
}
}

View file

@ -1,19 +1,58 @@
using Ryujinx.Common.Memory;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Shader
{
/// <summary>
/// Transform feedback descriptor.
/// </summary>
struct TransformFeedbackDescriptor
{
public int BufferIndex { get; }
public int Stride { get; }
// New fields should be added to the end of the struct to keep disk shader cache compatibility.
public byte[] VaryingLocations { get; }
/// <summary>
/// Index of the transform feedback.
/// </summary>
public readonly int BufferIndex;
public TransformFeedbackDescriptor(int bufferIndex, int stride, byte[] varyingLocations)
/// <summary>
/// Amount of bytes consumed per vertex.
/// </summary>
public readonly int Stride;
/// <summary>
/// Number of varyings written into the buffer.
/// </summary>
public readonly int VaryingCount;
/// <summary>
/// Location of varyings to be written into the buffer. Each byte is one location.
/// </summary>
public Array32<uint> VaryingLocations; // Making this readonly breaks AsSpan
/// <summary>
/// Creates a new transform feedback descriptor.
/// </summary>
/// <param name="bufferIndex">Index of the transform feedback</param>
/// <param name="stride">Amount of bytes consumed per vertex</param>
/// <param name="varyingCount">Number of varyings written into the buffer. Indicates size in bytes of <paramref name="varyingLocations"/></param>
/// <param name="varyingLocations">Location of varyings to be written into the buffer. Each byte is one location</param>
public TransformFeedbackDescriptor(int bufferIndex, int stride, int varyingCount, ref Array32<uint> varyingLocations)
{
BufferIndex = bufferIndex;
Stride = stride;
VaryingLocations = varyingLocations ?? throw new ArgumentNullException(nameof(varyingLocations));
BufferIndex = bufferIndex;
Stride = stride;
VaryingCount = varyingCount;
VaryingLocations = varyingLocations;
}
/// <summary>
/// Gets a span of the <see cref="VaryingLocations"/>.
/// </summary>
/// <returns>Span of varying locations</returns>
public ReadOnlySpan<byte> AsSpan()
{
return MemoryMarshal.Cast<uint, byte>(VaryingLocations.ToSpan()).Slice(0, Math.Min(128, VaryingCount));
}
}
}

View file

@ -1,6 +1,7 @@
using OpenTK.Graphics.OpenGL;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader;
namespace Ryujinx.Graphics.OpenGL
{
@ -528,5 +529,19 @@ namespace Ryujinx.Graphics.OpenGL
return All.Never;
}
public static ShaderType Convert(this ShaderStage stage)
{
return stage switch
{
ShaderStage.Compute => ShaderType.ComputeShader,
ShaderStage.Vertex => ShaderType.VertexShader,
ShaderStage.TessellationControl => ShaderType.TessControlShader,
ShaderStage.TessellationEvaluation => ShaderType.TessEvaluationShader,
ShaderStage.Geometry => ShaderType.GeometryShader,
ShaderStage.Fragment => ShaderType.FragmentShader,
_ => ShaderType.VertexShader
};
}
}
}

View file

@ -1,6 +1,8 @@
using OpenTK.Graphics.OpenGL;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader;
using Ryujinx.Graphics.Shader.Translation;
using System;
using System.Buffers.Binary;
@ -24,46 +26,66 @@ namespace Ryujinx.Graphics.OpenGL
}
private ProgramLinkStatus _status = ProgramLinkStatus.Incomplete;
private IShader[] _shaders;
private int[] _shaderHandles;
public bool HasFragmentShader;
public int FragmentOutputMap { get; }
public Program(IShader[] shaders, int fragmentOutputMap)
public Program(ShaderSource[] shaders, int fragmentOutputMap)
{
Handle = GL.CreateProgram();
GL.ProgramParameter(Handle, ProgramParameterName.ProgramBinaryRetrievableHint, 1);
_shaderHandles = new int[shaders.Length];
for (int index = 0; index < shaders.Length; index++)
{
Shader shader = (Shader)shaders[index];
ShaderSource shader = shaders[index];
if (shader.IsFragment)
if (shader.Stage == ShaderStage.Fragment)
{
HasFragmentShader = true;
}
GL.AttachShader(Handle, shader.Handle);
int shaderHandle = GL.CreateShader(shader.Stage.Convert());
switch (shader.Language)
{
case TargetLanguage.Glsl:
GL.ShaderSource(shaderHandle, shader.Code);
GL.CompileShader(shaderHandle);
break;
case TargetLanguage.Spirv:
GL.ShaderBinary(1, ref shaderHandle, (BinaryFormat)All.ShaderBinaryFormatSpirVArb, shader.BinaryCode, shader.BinaryCode.Length);
GL.SpecializeShader(shaderHandle, "main", 0, (int[])null, (int[])null);
break;
}
GL.AttachShader(Handle, shaderHandle);
_shaderHandles[index] = shaderHandle;
}
GL.LinkProgram(Handle);
_shaders = shaders;
FragmentOutputMap = fragmentOutputMap;
}
public Program(ReadOnlySpan<byte> code, bool hasFragmentShader, int fragmentOutputMap)
{
BinaryFormat binaryFormat = (BinaryFormat)BinaryPrimitives.ReadInt32LittleEndian(code.Slice(code.Length - 4, 4));
Handle = GL.CreateProgram();
unsafe
if (code.Length >= 4)
{
fixed (byte* ptr = code)
BinaryFormat binaryFormat = (BinaryFormat)BinaryPrimitives.ReadInt32LittleEndian(code.Slice(code.Length - 4, 4));
unsafe
{
GL.ProgramBinary(Handle, binaryFormat, (IntPtr)ptr, code.Length - 4);
fixed (byte* ptr = code)
{
GL.ProgramBinary(Handle, binaryFormat, (IntPtr)ptr, code.Length - 4);
}
}
}
@ -89,18 +111,7 @@ namespace Ryujinx.Graphics.OpenGL
}
GL.GetProgram(Handle, GetProgramParameterName.LinkStatus, out int status);
if (_shaders != null)
{
for (int index = 0; index < _shaders.Length; index++)
{
int shaderHandle = ((Shader)_shaders[index]).Handle;
GL.DetachShader(Handle, shaderHandle);
}
_shaders = null;
}
DeleteShaders();
if (status == 0)
{
@ -129,10 +140,25 @@ namespace Ryujinx.Graphics.OpenGL
return data;
}
private void DeleteShaders()
{
if (_shaderHandles != null)
{
foreach (int shaderHandle in _shaderHandles)
{
GL.DetachShader(Handle, shaderHandle);
GL.DeleteShader(shaderHandle);
}
_shaderHandles = null;
}
}
public void Dispose()
{
if (Handle != 0)
{
DeleteShaders();
GL.DeleteProgram(Handle);
Handle = 0;

View file

@ -1,11 +1,10 @@
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
using OpenTK.Graphics.OpenGL;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.OpenGL.Image;
using Ryujinx.Graphics.OpenGL.Queries;
using Ryujinx.Graphics.Shader;
using Ryujinx.Graphics.Shader.Translation;
using System;
namespace Ryujinx.Graphics.OpenGL
@ -54,11 +53,6 @@ namespace Ryujinx.Graphics.OpenGL
ResourcePool = new ResourcePool();
}
public IShader CompileShader(ShaderStage stage, string code)
{
return new Shader(stage, code);
}
public BufferHandle CreateBuffer(int size)
{
BufferCount++;
@ -66,7 +60,7 @@ namespace Ryujinx.Graphics.OpenGL
return Buffer.Create(size);
}
public IProgram CreateProgram(IShader[] shaders, ShaderInfo info)
public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info)
{
return new Program(shaders, info.FragmentOutputMap);
}
@ -101,6 +95,8 @@ namespace Ryujinx.Graphics.OpenGL
public Capabilities GetCapabilities()
{
return new Capabilities(
api: TargetApi.OpenGL,
vendorName: GpuVendor,
hasFrontFacingBug: HwCapabilities.Vendor == HwCapabilities.GpuVendor.IntelWindows,
hasVectorIndexingBug: HwCapabilities.Vendor == HwCapabilities.GpuVendor.AmdWindows,
supportsAstcCompression: HwCapabilities.SupportsAstcCompression,

View file

@ -1,42 +0,0 @@
using OpenTK.Graphics.OpenGL;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader;
namespace Ryujinx.Graphics.OpenGL
{
class Shader : IShader
{
public int Handle { get; private set; }
public bool IsFragment { get; }
public Shader(ShaderStage stage, string code)
{
ShaderType type = stage switch
{
ShaderStage.Compute => ShaderType.ComputeShader,
ShaderStage.Vertex => ShaderType.VertexShader,
ShaderStage.TessellationControl => ShaderType.TessControlShader,
ShaderStage.TessellationEvaluation => ShaderType.TessEvaluationShader,
ShaderStage.Geometry => ShaderType.GeometryShader,
ShaderStage.Fragment => ShaderType.FragmentShader,
_ => ShaderType.VertexShader
};
Handle = GL.CreateShader(type);
IsFragment = stage == ShaderStage.Fragment;
GL.ShaderSource(Handle, code);
GL.CompileShader(Handle);
}
public void Dispose()
{
if (Handle != 0)
{
GL.DeleteShader(Handle);
Handle = 0;
}
}
}
}

View file

@ -2,6 +2,8 @@ namespace Ryujinx.Graphics.Shader
{
public struct BufferDescriptor
{
// New fields should be added to the end of the struct to keep disk shader cache compatibility.
public readonly int Binding;
public readonly int Slot;
public BufferUsageFlags Flags;

View file

@ -373,7 +373,7 @@ namespace Ryujinx.Graphics.Shader.Decoders
for (int i = 0; i < cbOffsetsCount; i++)
{
uint targetOffset = config.GpuAccessor.ConstantBuffer1Read(cbBaseOffset + i * 4);
uint targetOffset = config.ConstantBuffer1Read(cbBaseOffset + i * 4);
Block target = getBlock(baseOffset + targetOffset);
target.Predecessors.Add(block);
block.Successors.Add(target);

View file

@ -2,153 +2,341 @@
namespace Ryujinx.Graphics.Shader
{
/// <summary>
/// GPU state access interface.
/// </summary>
public interface IGpuAccessor
{
/// <summary>
/// Prints a log message.
/// </summary>
/// <param name="message">Message to print</param>
void Log(string message)
{
// No default log output.
}
/// <summary>
/// Reads data from the constant buffer 1.
/// </summary>
/// <param name="offset">Offset in bytes to read from</param>
/// <returns>Value at the given offset</returns>
uint ConstantBuffer1Read(int offset)
{
return 0;
}
/// <summary>
/// Gets a span of the specified memory location, containing shader code.
/// </summary>
/// <param name="address">GPU virtual address of the data</param>
/// <param name="minimumSize">Minimum size that the returned span may have</param>
/// <returns>Span of the memory location</returns>
ReadOnlySpan<ulong> GetCode(ulong address, int minimumSize);
/// <summary>
/// Queries the binding number of a constant buffer.
/// </summary>
/// <param name="index">Constant buffer index</param>
/// <returns>Binding number</returns>
int QueryBindingConstantBuffer(int index)
{
return index;
}
/// <summary>
/// Queries the binding number of a storage buffer.
/// </summary>
/// <param name="index">Storage buffer index</param>
/// <returns>Binding number</returns>
int QueryBindingStorageBuffer(int index)
{
return index;
}
/// <summary>
/// Queries the binding number of a texture.
/// </summary>
/// <param name="index">Texture index</param>
/// <returns>Binding number</returns>
int QueryBindingTexture(int index)
{
return index;
}
/// <summary>
/// Queries the binding number of an image.
/// </summary>
/// <param name="index">Image index</param>
/// <returns>Binding number</returns>
int QueryBindingImage(int index)
{
return index;
}
/// <summary>
/// Queries Local Size X for compute shaders.
/// </summary>
/// <returns>Local Size X</returns>
int QueryComputeLocalSizeX()
{
return 1;
}
/// <summary>
/// Queries Local Size Y for compute shaders.
/// </summary>
/// <returns>Local Size Y</returns>
int QueryComputeLocalSizeY()
{
return 1;
}
/// <summary>
/// Queries Local Size Z for compute shaders.
/// </summary>
/// <returns>Local Size Z</returns>
int QueryComputeLocalSizeZ()
{
return 1;
}
/// <summary>
/// Queries Local Memory size in bytes for compute shaders.
/// </summary>
/// <returns>Local Memory size in bytes</returns>
int QueryComputeLocalMemorySize()
{
return 0x1000;
}
/// <summary>
/// Queries Shared Memory size in bytes for compute shaders.
/// </summary>
/// <returns>Shared Memory size in bytes</returns>
int QueryComputeSharedMemorySize()
{
return 0xc000;
}
/// <summary>
/// Queries Constant Buffer usage information.
/// </summary>
/// <returns>A mask where each bit set indicates a bound constant buffer</returns>
uint QueryConstantBufferUse()
{
return 0;
}
/// <summary>
/// Queries host about the presence of the FrontFacing built-in variable bug.
/// </summary>
/// <returns>True if the bug is present on the host device used, false otherwise</returns>
bool QueryHostHasFrontFacingBug()
{
return false;
}
/// <summary>
/// Queries host about the presence of the vector indexing bug.
/// </summary>
/// <returns>True if the bug is present on the host device used, false otherwise</returns>
bool QueryHostHasVectorIndexingBug()
{
return false;
}
/// <summary>
/// Queries host storage buffer alignment required.
/// </summary>
/// <returns>Host storage buffer alignment in bytes</returns>
int QueryHostStorageBufferOffsetAlignment()
{
return 16;
}
/// <summary>
/// Queries host support for texture formats with BGRA component order (such as BGRA8).
/// </summary>
/// <returns>True if BGRA formats are supported, false otherwise</returns>
bool QueryHostSupportsBgraFormat()
{
return true;
}
/// <summary>
/// Queries host support for fragment shader ordering critical sections on the shader code.
/// </summary>
/// <returns>True if fragment shader interlock is supported, false otherwise</returns>
bool QueryHostSupportsFragmentShaderInterlock()
{
return true;
}
/// <summary>
/// Queries host support for fragment shader ordering scoped critical sections on the shader code.
/// </summary>
/// <returns>True if fragment shader ordering is supported, false otherwise</returns>
bool QueryHostSupportsFragmentShaderOrderingIntel()
{
return false;
}
/// <summary>
/// Queries host support for readable images without a explicit format declaration on the shader.
/// </summary>
/// <returns>True if formatted image load is supported, false otherwise</returns>
bool QueryHostSupportsImageLoadFormatted()
{
return true;
}
/// <summary>
/// Queries host GPU non-constant texture offset support.
/// </summary>
/// <returns>True if the GPU and driver supports non-constant texture offsets, false otherwise</returns>
bool QueryHostSupportsNonConstantTextureOffset()
{
return true;
}
/// <summary>
/// Queries host GPU shader ballot support.
/// </summary>
/// <returns>True if the GPU and driver supports shader ballot, false otherwise</returns>
bool QueryHostSupportsShaderBallot()
{
return true;
}
/// <summary>
/// Queries host GPU texture shadow LOD support.
/// </summary>
/// <returns>True if the GPU and driver supports texture shadow LOD, false otherwise</returns>
bool QueryHostSupportsTextureShadowLod()
{
return true;
}
/// <summary>
/// Queries sampler type information.
/// </summary>
/// <param name="handle">Texture handle</param>
/// <param name="cbufSlot">Constant buffer slot for the texture handle</param>
/// <returns>The sampler type value for the given handle</returns>
SamplerType QuerySamplerType(int handle, int cbufSlot = -1)
{
return SamplerType.Texture2D;
}
bool QueryIsTextureRectangle(int handle, int cbufSlot = -1)
/// <summary>
/// Queries texture coordinate normalization information.
/// </summary>
/// <param name="handle">Texture handle</param>
/// <param name="cbufSlot">Constant buffer slot for the texture handle</param>
/// <returns>True if the coordinates are normalized, false otherwise</returns>
bool QueryTextureCoordNormalized(int handle, int cbufSlot = -1)
{
return false;
}
/// <summary>
/// Queries current primitive topology for geometry shaders.
/// </summary>
/// <returns>Current primitive topology</returns>
InputTopology QueryPrimitiveTopology()
{
return InputTopology.Points;
}
/// <summary>
/// Queries the tessellation evaluation shader primitive winding order.
/// </summary>
/// <returns>True if the primitive winding order is clockwise, false if counter-clockwise</returns>
bool QueryTessCw()
{
return false;
}
/// <summary>
/// Queries the tessellation evaluation shader abstract patch type.
/// </summary>
/// <returns>Abstract patch type</returns>
TessPatchType QueryTessPatchType()
{
return TessPatchType.Triangles;
}
/// <summary>
/// Queries the tessellation evaluation shader spacing between tessellated vertices of the patch.
/// </summary>
/// <returns>Spacing between tessellated vertices of the patch</returns>
TessSpacing QueryTessSpacing()
{
return TessSpacing.EqualSpacing;
}
/// <summary>
/// Queries texture format information, for shaders using image load or store.
/// </summary>
/// <remarks>
/// This only returns non-compressed color formats.
/// If the format of the texture is a compressed, depth or unsupported format, then a default value is returned.
/// </remarks>
/// <param name="handle">Texture handle</param>
/// <param name="cbufSlot">Constant buffer slot for the texture handle</param>
/// <returns>Color format of the non-compressed texture</returns>
TextureFormat QueryTextureFormat(int handle, int cbufSlot = -1)
{
return TextureFormat.R8G8B8A8Unorm;
}
/// <summary>
/// Queries transform feedback enable state.
/// </summary>
/// <returns>True if the shader uses transform feedback, false otherwise</returns>
bool QueryTransformFeedbackEnabled()
{
return false;
}
/// <summary>
/// Queries the varying locations that should be written to the transform feedback buffer.
/// </summary>
/// <param name="bufferIndex">Index of the transform feedback buffer</param>
/// <returns>Varying locations for the specified buffer</returns>
ReadOnlySpan<byte> QueryTransformFeedbackVaryingLocations(int bufferIndex)
{
return ReadOnlySpan<byte>.Empty;
}
/// <summary>
/// Queries the stride (in bytes) of the per vertex data written into the transform feedback buffer.
/// </summary>
/// <param name="bufferIndex">Index of the transform feedback buffer</param>
/// <returns>Stride for the specified buffer</returns>
int QueryTransformFeedbackStride(int bufferIndex)
{
return 0;
}
/// <summary>
/// Queries if host state forces early depth testing.
/// </summary>
/// <returns>True if early depth testing is forced</returns>
bool QueryEarlyZForce()
{
return false;
}
/// <summary>
/// Registers a texture used by the shader.
/// </summary>
/// <param name="handle">Texture handle word offset</param>
/// <param name="cbufSlot">Constant buffer slot where the texture handle is located</param>
void RegisterTexture(int handle, int cbufSlot)
{
// Only useful when recording information for a disk shader cache.
}
}
}

View file

@ -1,25 +1,28 @@
using Ryujinx.Graphics.Shader.Translation;
using System;
namespace Ryujinx.Graphics.Shader
{
public class ShaderProgram
{
public ShaderStage Stage { get; }
public ShaderProgramInfo Info { get; }
public TargetLanguage Language { get; }
public string Code { get; private set; }
public byte[] BinaryCode { get; }
private ShaderProgram(ShaderStage stage)
private ShaderProgram(ShaderProgramInfo info, TargetLanguage language)
{
Stage = stage;
Info = info;
Language = language;
}
public ShaderProgram(ShaderStage stage, string code) : this(stage)
public ShaderProgram(ShaderProgramInfo info, TargetLanguage language, string code) : this(info, language)
{
Code = code;
}
public ShaderProgram(ShaderStage stage, byte[] binaryCode) : this(stage)
public ShaderProgram(ShaderProgramInfo info, TargetLanguage language, byte[] binaryCode) : this(info, language)
{
BinaryCode = binaryCode;
}

View file

@ -10,6 +10,7 @@ namespace Ryujinx.Graphics.Shader
public ReadOnlyCollection<TextureDescriptor> Textures { get; }
public ReadOnlyCollection<TextureDescriptor> Images { get; }
public ShaderStage Stage { get; }
public bool UsesInstanceId { get; }
public bool UsesRtLayer { get; }
public byte ClipDistancesWritten { get; }
@ -20,6 +21,7 @@ namespace Ryujinx.Graphics.Shader
BufferDescriptor[] sBuffers,
TextureDescriptor[] textures,
TextureDescriptor[] images,
ShaderStage stage,
bool usesInstanceId,
bool usesRtLayer,
byte clipDistancesWritten,
@ -30,6 +32,7 @@ namespace Ryujinx.Graphics.Shader
Textures = Array.AsReadOnly(textures);
Images = Array.AsReadOnly(images);
Stage = stage;
UsesInstanceId = usesInstanceId;
UsesRtLayer = usesRtLayer;
ClipDistancesWritten = clipDistancesWritten;

View file

@ -74,7 +74,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
for (int j = 0; j < locations.Length; j++)
{
byte location = locations[j];
if (location < 0x80)
if (location < 0xc0)
{
context.Info.TransformFeedbackOutputs[location] = new TransformFeedbackOutput(tfbIndex, j * 4, stride);
}

View file

@ -30,7 +30,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
{
Functions = new List<StructuredFunction>();
TransformFeedbackOutputs = new TransformFeedbackOutput[0x80];
TransformFeedbackOutputs = new TransformFeedbackOutput[0xc0];
}
}
}

View file

@ -2,6 +2,8 @@ namespace Ryujinx.Graphics.Shader
{
public struct TextureDescriptor
{
// New fields should be added to the end of the struct to keep disk shader cache compatibility.
public readonly int Binding;
public readonly SamplerType Type;

View file

@ -164,9 +164,9 @@ namespace Ryujinx.Graphics.Shader.Translation
bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
bool isRect = !isBindless && config.GpuAccessor.QueryIsTextureRectangle(texOp.Handle, texOp.CbufSlot);
bool isCoordNormalized = !isBindless && config.GpuAccessor.QueryTextureCoordNormalized(texOp.Handle, texOp.CbufSlot);
if (!(hasInvalidOffset || isRect))
if (!hasInvalidOffset && isCoordNormalized)
{
return node;
}
@ -263,7 +263,7 @@ namespace Ryujinx.Graphics.Shader.Translation
hasInvalidOffset &= !areAllOffsetsConstant;
if (!(hasInvalidOffset || isRect))
if (!hasInvalidOffset && isCoordNormalized)
{
return node;
}
@ -300,15 +300,17 @@ namespace Ryujinx.Graphics.Shader.Translation
return res;
}
// Emulate texture rectangle by normalizing the coordinates on the shader.
// When sampler*Rect is used, the coords are expected to the in the [0, W or H] range,
// Emulate non-normalized coordinates by normalizing the coordinates on the shader.
// Without normalization, the coordinates are expected to the in the [0, W or H] range,
// and otherwise, it is expected to be in the [0, 1] range.
// We normalize by dividing the coords by the texture size.
if (isRect && !intCoords)
if (!isCoordNormalized && !intCoords)
{
config.SetUsedFeature(FeatureFlags.IntegerSampling);
for (int index = 0; index < coordsCount; index++)
int normCoordsCount = (texOp.Type & SamplerType.Mask) == SamplerType.TextureCube ? 2 : coordsCount;
for (int index = 0; index < normCoordsCount; index++)
{
Operand coordSize = Local();

View file

@ -41,9 +41,7 @@ namespace Ryujinx.Graphics.Shader.Translation
public FeatureFlags UsedFeatures { get; private set; }
public HashSet<int> TextureHandlesForCache { get; }
private readonly TranslationCounts _counts;
public int Cb1DataSize { get; private set; }
public bool NextUsesFixedFuncAttributes { get; private set; }
public int UsedInputAttributes { get; private set; }
@ -109,21 +107,22 @@ namespace Ryujinx.Graphics.Shader.Translation
private TextureDescriptor[] _cachedTextureDescriptors;
private TextureDescriptor[] _cachedImageDescriptors;
public int FirstConstantBufferBinding { get; private set; }
public int FirstStorageBufferBinding { get; private set; }
private int _firstConstantBufferBinding;
private int _firstStorageBufferBinding;
public ShaderConfig(IGpuAccessor gpuAccessor, TranslationOptions options, TranslationCounts counts)
public int FirstConstantBufferBinding => _firstConstantBufferBinding;
public int FirstStorageBufferBinding => _firstStorageBufferBinding;
public ShaderConfig(IGpuAccessor gpuAccessor, TranslationOptions options)
{
Stage = ShaderStage.Compute;
GpuAccessor = gpuAccessor;
Options = options;
_counts = counts;
TextureHandlesForCache = new HashSet<int>();
_usedTextures = new Dictionary<TextureInfo, TextureMeta>();
_usedImages = new Dictionary<TextureInfo, TextureMeta>();
Stage = ShaderStage.Compute;
GpuAccessor = gpuAccessor;
Options = options;
_usedTextures = new Dictionary<TextureInfo, TextureMeta>();
_usedImages = new Dictionary<TextureInfo, TextureMeta>();
}
public ShaderConfig(ShaderHeader header, IGpuAccessor gpuAccessor, TranslationOptions options, TranslationCounts counts) : this(gpuAccessor, options, counts)
public ShaderConfig(ShaderHeader header, IGpuAccessor gpuAccessor, TranslationOptions options) : this(gpuAccessor, options)
{
Stage = header.Stage;
GpPassthrough = header.Stage == ShaderStage.Geometry && header.GpPassthrough;
@ -144,6 +143,16 @@ namespace Ryujinx.Graphics.Shader.Translation
return BitOperations.PopCount((uint)OmapTargets) + 1;
}
public uint ConstantBuffer1Read(int offset)
{
if (Cb1DataSize < offset + 4)
{
Cb1DataSize = offset + 4;
}
return GpuAccessor.ConstantBuffer1Read(offset);
}
public TextureFormat GetTextureFormat(int handle, int cbufSlot = -1)
{
// When the formatted load extension is supported, we don't need to
@ -197,8 +206,6 @@ namespace Ryujinx.Graphics.Shader.Translation
ClipDistancesWritten |= other.ClipDistancesWritten;
UsedFeatures |= other.UsedFeatures;
TextureHandlesForCache.UnionWith(other.TextureHandlesForCache);
UsedInputAttributes |= other.UsedInputAttributes;
UsedOutputAttributes |= other.UsedOutputAttributes;
_usedConstantBuffers |= other._usedConstantBuffers;
@ -391,6 +398,8 @@ namespace Ryujinx.Graphics.Shader.Translation
bool intCoords = flags.HasFlag(TextureFlags.IntCoords) || inst == Instruction.TextureSize;
SetUsedTextureOrImage(_usedTextures, cbufSlot, handle, type, TextureFormat.Unknown, intCoords, false, accurateType, coherent);
}
GpuAccessor.RegisterTexture(handle, cbufSlot);
}
private void SetUsedTextureOrImage(
@ -485,13 +494,12 @@ namespace Ryujinx.Graphics.Shader.Translation
usedMask |= (int)GpuAccessor.QueryConstantBufferUse();
}
FirstConstantBufferBinding = _counts.UniformBuffersCount;
return _cachedConstantBufferDescriptors = GetBufferDescriptors(
usedMask,
0,
UsedFeatures.HasFlag(FeatureFlags.CbIndexing),
_counts.IncrementUniformBuffersCount);
out _firstConstantBufferBinding,
GpuAccessor.QueryBindingConstantBuffer);
}
public BufferDescriptor[] GetStorageBufferDescriptors()
@ -501,21 +509,23 @@ namespace Ryujinx.Graphics.Shader.Translation
return _cachedStorageBufferDescriptors;
}
FirstStorageBufferBinding = _counts.StorageBuffersCount;
return _cachedStorageBufferDescriptors = GetBufferDescriptors(
_usedStorageBuffers,
_usedStorageBuffersWrite,
true,
_counts.IncrementStorageBuffersCount);
out _firstStorageBufferBinding,
GpuAccessor.QueryBindingStorageBuffer);
}
private static BufferDescriptor[] GetBufferDescriptors(
int usedMask,
int writtenMask,
bool isArray,
Func<int> getBindingCallback)
out int firstBinding,
Func<int, int> getBindingCallback)
{
firstBinding = 0;
bool hasFirstBinding = false;
var descriptors = new BufferDescriptor[BitOperations.PopCount((uint)usedMask)];
int lastSlot = -1;
@ -529,13 +539,25 @@ namespace Ryujinx.Graphics.Shader.Translation
// The next array entries also consumes bindings, even if they are unused.
for (int j = lastSlot + 1; j < slot; j++)
{
getBindingCallback();
int binding = getBindingCallback(j);
if (!hasFirstBinding)
{
firstBinding = binding;
hasFirstBinding = true;
}
}
}
lastSlot = slot;
descriptors[i] = new BufferDescriptor(getBindingCallback(), slot);
descriptors[i] = new BufferDescriptor(getBindingCallback(slot), slot);
if (!hasFirstBinding)
{
firstBinding = descriptors[i].Binding;
hasFirstBinding = true;
}
if ((writtenMask & (1 << slot)) != 0)
{
@ -550,15 +572,15 @@ namespace Ryujinx.Graphics.Shader.Translation
public TextureDescriptor[] GetTextureDescriptors()
{
return _cachedTextureDescriptors ??= GetTextureOrImageDescriptors(_usedTextures, _counts.IncrementTexturesCount);
return _cachedTextureDescriptors ??= GetTextureOrImageDescriptors(_usedTextures, GpuAccessor.QueryBindingTexture);
}
public TextureDescriptor[] GetImageDescriptors()
{
return _cachedImageDescriptors ??= GetTextureOrImageDescriptors(_usedImages, _counts.IncrementImagesCount);
return _cachedImageDescriptors ??= GetTextureOrImageDescriptors(_usedImages, GpuAccessor.QueryBindingImage);
}
private static TextureDescriptor[] GetTextureOrImageDescriptors(Dictionary<TextureInfo, TextureMeta> dict, Func<int> getBindingCallback)
private static TextureDescriptor[] GetTextureOrImageDescriptors(Dictionary<TextureInfo, TextureMeta> dict, Func<int, int> getBindingCallback)
{
var descriptors = new TextureDescriptor[dict.Count];
@ -568,7 +590,7 @@ namespace Ryujinx.Graphics.Shader.Translation
var info = kv.Key;
var meta = kv.Value;
int binding = getBindingCallback();
int binding = getBindingCallback(i);
descriptors[i] = new TextureDescriptor(binding, meta.Type, info.Format, info.CbufSlot, info.Handle);
descriptors[i].SetFlag(meta.UsageFlags);

View file

@ -1,36 +0,0 @@
namespace Ryujinx.Graphics.Shader.Translation
{
public class TranslationCounts
{
public int UniformBuffersCount { get; private set; }
public int StorageBuffersCount { get; private set; }
public int TexturesCount { get; private set; }
public int ImagesCount { get; private set; }
public TranslationCounts()
{
// The first binding is reserved for the support buffer.
UniformBuffersCount = 1;
}
internal int IncrementUniformBuffersCount()
{
return UniformBuffersCount++;
}
internal int IncrementStorageBuffersCount()
{
return StorageBuffersCount++;
}
internal int IncrementTexturesCount()
{
return TexturesCount++;
}
internal int IncrementImagesCount()
{
return ImagesCount++;
}
}
}

View file

@ -25,18 +25,12 @@ namespace Ryujinx.Graphics.Shader.Translation
}
}
public static TranslatorContext CreateContext(
ulong address,
IGpuAccessor gpuAccessor,
TranslationOptions options,
TranslationCounts counts = null)
public static TranslatorContext CreateContext(ulong address, IGpuAccessor gpuAccessor, TranslationOptions options)
{
counts ??= new TranslationCounts();
return DecodeShader(address, gpuAccessor, options, counts);
return DecodeShader(address, gpuAccessor, options);
}
internal static ShaderProgram Translate(FunctionCode[] functions, ShaderConfig config, out ShaderProgramInfo shaderProgramInfo)
internal static ShaderProgram Translate(FunctionCode[] functions, ShaderConfig config)
{
var cfgs = new ControlFlowGraph[functions.Length];
var frus = new RegisterUsage.FunctionRegisterUsage[functions.Length];
@ -87,31 +81,25 @@ namespace Ryujinx.Graphics.Shader.Translation
StructuredProgramInfo sInfo = StructuredProgram.MakeStructuredProgram(funcs, config);
ShaderProgram program;
switch (config.Options.TargetLanguage)
{
case TargetLanguage.Glsl:
program = new ShaderProgram(config.Stage, GlslGenerator.Generate(sInfo, config));
break;
default:
throw new NotImplementedException(config.Options.TargetLanguage.ToString());
}
shaderProgramInfo = new ShaderProgramInfo(
ShaderProgramInfo info = new ShaderProgramInfo(
config.GetConstantBufferDescriptors(),
config.GetStorageBufferDescriptors(),
config.GetTextureDescriptors(),
config.GetImageDescriptors(),
config.Stage,
config.UsedFeatures.HasFlag(FeatureFlags.InstanceId),
config.UsedFeatures.HasFlag(FeatureFlags.RtLayer),
config.ClipDistancesWritten,
config.OmapTargets);
return program;
return config.Options.TargetLanguage switch
{
TargetLanguage.Glsl => new ShaderProgram(info, TargetLanguage.Glsl, GlslGenerator.Generate(sInfo, config)),
_ => throw new NotImplementedException(config.Options.TargetLanguage.ToString())
};
}
private static TranslatorContext DecodeShader(ulong address, IGpuAccessor gpuAccessor, TranslationOptions options, TranslationCounts counts)
private static TranslatorContext DecodeShader(ulong address, IGpuAccessor gpuAccessor, TranslationOptions options)
{
ShaderConfig config;
DecodedProgram program;
@ -119,13 +107,13 @@ namespace Ryujinx.Graphics.Shader.Translation
if ((options.Flags & TranslationFlags.Compute) != 0)
{
config = new ShaderConfig(gpuAccessor, options, counts);
config = new ShaderConfig(gpuAccessor, options);
program = Decoder.Decode(config, address);
}
else
{
config = new ShaderConfig(new ShaderHeader(gpuAccessor, address), gpuAccessor, options, counts);
config = new ShaderConfig(new ShaderHeader(gpuAccessor, address), gpuAccessor, options);
program = Decoder.Decode(config, address + HeaderSize);
}
@ -138,20 +126,6 @@ namespace Ryujinx.Graphics.Shader.Translation
{
maxEndAddress = block.EndAddress;
}
if (!config.UsedFeatures.HasFlag(FeatureFlags.Bindless))
{
for (int index = 0; index < block.OpCodes.Count; index++)
{
InstOp op = block.OpCodes[index];
if (op.Props.HasFlag(InstProps.Tex))
{
int tidB = (int)((op.RawOpCode >> 36) & 0x1fff);
config.TextureHandlesForCache.Add(tidB);
}
}
}
}
}

View file

@ -16,10 +16,7 @@ namespace Ryujinx.Graphics.Shader.Translation
public ShaderStage Stage => _config.Stage;
public int Size => _config.Size;
public FeatureFlags UsedFeatures => _config.UsedFeatures;
public HashSet<int> TextureHandlesForCache => _config.TextureHandlesForCache;
public int Cb1DataSize => _config.Cb1DataSize;
public IGpuAccessor GpuAccessor => _config.GpuAccessor;
@ -129,16 +126,13 @@ namespace Ryujinx.Graphics.Shader.Translation
return output;
}
public ShaderProgram Translate(
out ShaderProgramInfo shaderProgramInfo,
TranslatorContext nextStage = null,
TranslatorContext other = null)
public void SetNextStage(TranslatorContext nextStage)
{
if (nextStage != null)
{
_config.MergeFromtNextStage(nextStage._config);
}
_config.MergeFromtNextStage(nextStage._config);
}
public ShaderProgram Translate(TranslatorContext other = null)
{
FunctionCode[] code = EmitShader(_program, _config, initializeOutputs: other == null, out _);
if (other != null)
@ -152,7 +146,7 @@ namespace Ryujinx.Graphics.Shader.Translation
_config.InheritFrom(other._config);
}
return Translator.Translate(code, _config, out shaderProgramInfo);
return Translator.Translate(code, _config);
}
}
}

View file

@ -43,6 +43,7 @@ namespace Ryujinx.Headless.SDL2
private GraphicsDebugLevel _glLogLevel;
private readonly Stopwatch _chrono;
private readonly long _ticksPerFrame;
private readonly CancellationTokenSource _gpuCancellationTokenSource;
private readonly ManualResetEvent _exitEvent;
private long _ticks;
@ -66,6 +67,7 @@ namespace Ryujinx.Headless.SDL2
_glLogLevel = glLogLevel;
_chrono = new Stopwatch();
_ticksPerFrame = Stopwatch.Frequency / TargetFps;
_gpuCancellationTokenSource = new CancellationTokenSource();
_exitEvent = new ManualResetEvent(false);
_aspectRatio = aspectRatio;
_enableMouse = enableMouse;
@ -162,7 +164,7 @@ namespace Ryujinx.Headless.SDL2
Device.Gpu.Renderer.RunLoop(() =>
{
Device.Gpu.InitializeShaderCache();
Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
Translator.IsReadyForTranslation.Set();
while (_isActive)
@ -223,6 +225,8 @@ namespace Ryujinx.Headless.SDL2
return;
}
_gpuCancellationTokenSource.Cancel();
_isStopped = true;
_isActive = false;

View file

@ -55,7 +55,7 @@ namespace Ryujinx.ShaderTools
TranslationOptions translationOptions = new TranslationOptions(options.TargetLanguage, options.TargetApi, flags);
ShaderProgram program = Translator.CreateContext(0, new GpuAccessor(data), translationOptions).Translate(out _);
ShaderProgram program = Translator.CreateContext(0, new GpuAccessor(data), translationOptions).Translate();
if (options.OutputPath == null)
{

View file

@ -60,6 +60,8 @@ namespace Ryujinx.Ui
private readonly ManualResetEvent _exitEvent;
private readonly CancellationTokenSource _gpuCancellationTokenSource;
// Hide Cursor
const int CursorHideIdleTime = 8; // seconds
private static readonly Cursor _invisibleCursor = new Cursor(Display.Default, CursorType.BlankCursor);
@ -105,6 +107,8 @@ namespace Ryujinx.Ui
_exitEvent = new ManualResetEvent(false);
_gpuCancellationTokenSource = new CancellationTokenSource();
_hideCursorOnIdle = ConfigurationState.Instance.HideCursorOnIdle;
_lastCursorMoveTime = Stopwatch.GetTimestamp();
@ -387,7 +391,7 @@ namespace Ryujinx.Ui
Device.Gpu.Renderer.RunLoop(() =>
{
Device.Gpu.SetGpuThread();
Device.Gpu.InitializeShaderCache();
Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
Translator.IsReadyForTranslation.Set();
(Toplevel as MainWindow)?.ActivatePauseMenu();
@ -499,6 +503,8 @@ namespace Ryujinx.Ui
return;
}
_gpuCancellationTokenSource.Cancel();
_isStopped = true;
_isActive = false;
@ -603,7 +609,7 @@ namespace Ryujinx.Ui
if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleMute) &&
!_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleMute))
{
if (Device.IsAudioMuted())
if (Device.IsAudioMuted())
{
Device.SetVolume(ConfigurationState.Instance.System.AudioVolume);
}

View file

@ -172,7 +172,7 @@ namespace Ryujinx.Ui.Widgets
ResponseType response = (ResponseType)fileChooser.Run();
string destination = fileChooser.Filename;
fileChooser.Dispose();
if (response == ResponseType.Accept)
@ -490,7 +490,7 @@ namespace Ryujinx.Ui.Widgets
private void OpenPtcDir_Clicked(object sender, EventArgs args)
{
string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu");
string mainPath = System.IO.Path.Combine(ptcDir, "0");
string backupPath = System.IO.Path.Combine(ptcDir, "1");
@ -515,7 +515,7 @@ namespace Ryujinx.Ui.Widgets
OpenHelper.OpenFolder(shaderCacheDir);
}
private void PurgePtcCache_Clicked(object sender, EventArgs args)
{
DirectoryInfo mainDir = new DirectoryInfo(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "0"));
@ -526,7 +526,7 @@ namespace Ryujinx.Ui.Widgets
List<FileInfo> cacheFiles = new List<FileInfo>();
if (mainDir.Exists)
{
{
cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache"));
}
@ -539,9 +539,9 @@ namespace Ryujinx.Ui.Widgets
{
foreach (FileInfo file in cacheFiles)
{
try
{
file.Delete();
try
{
file.Delete();
}
catch(Exception e)
{
@ -557,18 +557,21 @@ namespace Ryujinx.Ui.Widgets
{
DirectoryInfo shaderCacheDir = new DirectoryInfo(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader"));
MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n<b>{_titleName}</b>\n\nAre you sure you want to proceed?");
using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n<b>{_titleName}</b>\n\nAre you sure you want to proceed?");
List<DirectoryInfo> cacheDirectory = new List<DirectoryInfo>();
List<DirectoryInfo> oldCacheDirectories = new List<DirectoryInfo>();
List<FileInfo> newCacheFiles = new List<FileInfo>();
if (shaderCacheDir.Exists)
{
cacheDirectory.AddRange(shaderCacheDir.EnumerateDirectories("*"));
oldCacheDirectories.AddRange(shaderCacheDir.EnumerateDirectories("*"));
newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.toc"));
newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.data"));
}
if (cacheDirectory.Count > 0 && warningDialog.Run() == (int)ResponseType.Yes)
if ((oldCacheDirectories.Count > 0 || newCacheFiles.Count > 0) && warningDialog.Run() == (int)ResponseType.Yes)
{
foreach (DirectoryInfo directory in cacheDirectory)
foreach (DirectoryInfo directory in oldCacheDirectories)
{
try
{
@ -579,9 +582,19 @@ namespace Ryujinx.Ui.Widgets
GtkDialog.CreateErrorDialog($"Error purging shader cache at {directory.Name}: {e}");
}
}
}
warningDialog.Dispose();
foreach (FileInfo file in newCacheFiles)
{
try
{
file.Delete();
}
catch (Exception e)
{
GtkDialog.CreateErrorDialog($"Error purging shader cache at {file.Name}: {e}");
}
}
}
}
}
}