Files
the-beast-roofer-edition/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
T
rooferdev 6634d3ef87 The Beast Roofer Edition v1.2.4
Removes a bug introduced in v1.2.3: the DLSS clip-space jitter block was
emitted into every vertex shader and read the Position output before
writing it. Reading an unwritten output is undefined, so vertex positions
could come out as garbage - whole 3D passes landing nowhere (the green
screen reported on GYLT, Ghostbusters, Blair Witch and others) and the
black block artifacts in Xenoblade Chronicles 2's motion-blur pass, which
had been mistaken for a long-standing emulation defect and worked around
by disabling the effect. With jitter off - how this fork ships - the
vertex epilogue is now identical to the base emulator again, and the
motion blur no longer needs to be disabled. CodeGenVersion bumped so
existing shader caches are rebuilt.

Also adds a DLSS sharpening slider (0-100, off by default) and an
experimental advanced motion stack checkbox (off by default).
2026-08-03 13:44:30 -04:00

918 lines
36 KiB
C#

using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader;
using Ryujinx.Graphics.Shader.Translation;
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 = 2;
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
// [JITTEREMIT 03/08] Le bloc de jitter clip-space n'est plus emis dans les vertex shaders
// quand le jitter est eteint (il lisait Position en sortie avant de l'ecrire = indefini).
// La TRADUCTION change => les caches de shaders existants doivent etre invalides, sinon
// les utilisateurs continuent d'executer l'ancien code et le correctif ne les atteint pas.
private const uint CodeGenVersion = 7355;
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>
/// Timestamp of when the file was first created.
/// </summary>
public ulong Timestamp;
/// <summary>
/// Reserved space, to be used in the future. Write as zero.
/// </summary>
public ulong Reserved;
}
/// <summary>
/// Offset and size pair.
/// </summary>
private struct OffsetAndSize
{
/// <summary>
/// Offset.
/// </summary>
public ulong Offset;
/// <summary>
/// Size of uncompressed data.
/// </summary>
public uint UncompressedSize;
/// <summary>
/// Size of compressed data.
/// </summary>
public uint CompressedSize;
}
/// <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>
/// Number of vertices that each output primitive has on a geometry shader.
/// </summary>
public byte GeometryVerticesPerPrimitive;
/// <summary>
/// Maximum number of vertices that a geometry shader may generate.
/// </summary>
public ushort GeometryMaxOutputVertices;
/// <summary>
/// Number of invocations per primitive on tessellation or geometry shaders.
/// </summary>
public ushort ThreadsPerInputPrimitive;
/// <summary>
/// Indicates if the fragment shader accesses the fragment coordinate built-in variable.
/// </summary>
public bool UsesFragCoord;
/// <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;
/// <summary>
/// Indicates if the vertex shader accesses draw parameters.
/// </summary>
public bool UsesDrawParameters;
}
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 Math.Max((int)((new FileInfo(tocFilePath).Length - Unsafe.SizeOf<TocHeader>()) / sizeof(ulong)), 0);
}
/// <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[..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 FileStream tocFileStream = DiskCacheCommon.OpenFile(_basePath, SharedTocFileName, writable: false);
using FileStream dataFileStream = DiskCacheCommon.OpenFile(_basePath, SharedDataFileName, writable: false);
using Stream guestTocFileStream = _guestStorage.OpenTocFileStream();
using Stream guestDataFileStream = _guestStorage.OpenDataFileStream();
BinarySerializer tocReader = new(tocFileStream);
BinarySerializer dataReader = new(dataFileStream);
TocHeader header = new();
if (!tocReader.TryRead(ref header) || header.Magic != TocsMagic)
{
throw new DiskCacheLoadException(DiskCacheLoadResult.FileCorruptedGeneric);
}
if (header.FormatVersion != FileFormatVersionPacked)
{
throw new DiskCacheLoadException(DiskCacheLoadResult.IncompatibleVersion);
}
// MV++ shader-codegen probes: any flag that changes or instruments the generated
// host code must take the guest-recompile path (the same one every codegen version
// bump takes). Loading would serve shaders built with the OTHER setting when the
// flag was toggled between launches -- the stale-instrumentation trap. The write
// side is guarded in AddShader for the same reason.
bool loadHostCache = header.CodeGenVersion == CodeGenVersion && !_mvppCodegenProbeActive;
int programIndex = 0;
DataEntry entry = new();
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;
}
GuestCodeAndCbData?[] guestShaders = new GuestCodeAndCbData?[isCompute ? 1 : Constants.ShaderStages + 1];
DataEntryPerStage stageEntry = new();
while (stagesBitMask != 0)
{
int stageIndex = BitOperations.TrailingZeroCount(stagesBitMask);
dataReader.Read(ref stageEntry);
guestShaders[stageIndex] = _guestStorage.LoadShader(
guestTocFileStream,
guestDataFileStream,
stageEntry.GuestCodeIndex);
stagesBitMask &= ~(1u << stageIndex);
}
ShaderSpecializationState specState = ShaderSpecializationState.Read(ref dataReader);
dataReader.EndCompression();
if (loadHostCache)
{
(byte[] hostCode, CachedShaderStage[] shaders) = ReadHostCode(
context,
ref hostTocFileStream,
ref hostDataFileStream,
guestShaders,
programIndex,
header.Timestamp);
if (hostCode != null)
{
ShaderInfo shaderInfo = ShaderInfoBuilder.BuildForCache(
context,
shaders,
specState.PipelineState,
specState.TransformFeedbackDescriptors != null);
IProgram hostProgram;
if (context.Capabilities.Api == TargetApi.Vulkan)
{
ShaderSource[] shaderSources = ShaderBinarySerializer.Unpack(shaders, hostCode);
hostProgram = context.Renderer.CreateProgram(shaderSources, shaderInfo);
}
else
{
bool hasFragmentShader = shaders.Length > 5 && shaders[5] != null;
hostProgram = context.Renderer.LoadProgramBinary(hostCode, hasFragmentShader, shaderInfo);
}
CachedShaderProgram program = new(hostProgram, specState, shaders);
loader.QueueHostProgram(program, hostCode, programIndex, isCompute);
}
else
{
loadHostCache = false;
}
}
if (!loadHostCache)
{
loader.QueueGuestProgram(guestShaders, 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="guestShaders">Guest shader code for each active stage</param>
/// <param name="programIndex">Index of the program on the cache</param>
/// <param name="expectedTimestamp">Timestamp of the shared cache file. The host file must be newer than it</param>
/// <returns>Host binary code, or null if not found</returns>
private (byte[], CachedShaderStage[]) ReadHostCode(
GpuContext context,
ref Stream tocFileStream,
ref Stream dataFileStream,
GuestCodeAndCbData?[] guestShaders,
int programIndex,
ulong expectedTimestamp)
{
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, null);
}
tocFileStream = DiskCacheCommon.OpenFile(_basePath, GetHostTocFileName(context), writable: false);
dataFileStream = DiskCacheCommon.OpenFile(_basePath, GetHostDataFileName(context), writable: false);
BinarySerializer tempTocReader = new(tocFileStream);
TocHeader header = new();
tempTocReader.Read(ref header);
if (header.Timestamp < expectedTimestamp)
{
return (null, null);
}
}
int offset = Unsafe.SizeOf<TocHeader>() + programIndex * Unsafe.SizeOf<OffsetAndSize>();
if (offset + Unsafe.SizeOf<OffsetAndSize>() > tocFileStream.Length)
{
return (null, null);
}
if ((ulong)offset >= (ulong)dataFileStream.Length)
{
throw new DiskCacheLoadException(DiskCacheLoadResult.FileCorruptedGeneric);
}
tocFileStream.Seek(offset, SeekOrigin.Begin);
BinarySerializer tocReader = new(tocFileStream);
OffsetAndSize offsetAndSize = new();
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.UncompressedSize];
BinarySerializer.ReadCompressed(dataFileStream, hostCode);
CachedShaderStage[] shaders = new CachedShaderStage[guestShaders.Length];
BinarySerializer dataReader = new(dataFileStream);
dataFileStream.Seek((long)(offsetAndSize.Offset + offsetAndSize.CompressedSize), SeekOrigin.Begin);
dataReader.BeginCompression();
for (int index = 0; index < guestShaders.Length; index++)
{
if (!guestShaders[index].HasValue)
{
continue;
}
GuestCodeAndCbData guestShader = guestShaders[index].Value;
ShaderProgramInfo info = index != 0 || guestShaders.Length == 1 ? ReadShaderProgramInfo(ref dataReader) : null;
shaders[index] = new CachedShaderStage(info, guestShader.Code, guestShader.Cb1Data);
}
dataReader.EndCompression();
return (hostCode, shaders);
}
/// <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)
{
FileStream tocFileStream = DiskCacheCommon.OpenFile(_basePath, SharedTocFileName, writable: true);
FileStream dataFileStream = DiskCacheCommon.OpenFile(_basePath, SharedDataFileName, writable: true);
FileStream hostTocFileStream = DiskCacheCommon.OpenFile(_basePath, GetHostTocFileName(context), writable: true);
FileStream 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>
// MV++ shader-codegen probes that alter or instrument the generated host code. While any
// of them is active the shared host cache must be neither loaded (stale code from the
// other setting) nor written (instrumented code would poison the next probe-less launch).
private static readonly bool _mvppCodegenProbeActive =
Ryujinx.Graphics.Shader.Translation.MvppPositionProbe.Enabled ||
Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.Enabled ||
Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.MipMarkEnabled ||
Ryujinx.Graphics.Shader.Translation.MvppSurviveProbe.BufferEnabled ||
Ryujinx.Graphics.Shader.Translation.MvppHashedAlpha.ProbeEnabled ||
Ryujinx.Graphics.Shader.Translation.MvppHashedAlpha.RewriteEnabled ||
Ryujinx.Graphics.Shader.Translation.MvppHashedAlpha.CaptureEnabled ||
Ryujinx.Graphics.Shader.Translation.MvppHashedAlpha.LodBiasEnabled ||
Ryujinx.Graphics.Shader.Translation.HashTestProbe.Enabled ||
Ryujinx.Graphics.Shader.Translation.MufuPrecProbe.Enabled ||
Ryujinx.Graphics.Shader.Translation.RroReduceProbe.Enabled ||
Ryujinx.Graphics.Shader.Translation.CocConstProbe.Enabled ||
Ryujinx.Graphics.Shader.Translation.TileMapConstProbe.Enabled ||
Ryujinx.Graphics.Shader.Translation.MvKillProbe.Enabled ||
Ryujinx.Graphics.Shader.Translation.MvResolveMvProbe.Enabled ||
Ryujinx.Graphics.Shader.Translation.MvppTileCapProbe.Enabled ||
Ryujinx.Graphics.Shader.Translation.MvppTruncEpsProbe.Enabled ||
Ryujinx.Graphics.Shader.Translation.MvppForceLod0Probe.Enabled ||
Ryujinx.Graphics.Shader.Translation.MvSignProbe.Enabled ||
Ryujinx.Graphics.Shader.Translation.SkyMvProbe.Enabled ||
Ryujinx.Graphics.Shader.Translation.NanScrubProbe.Enabled ||
Ryujinx.Graphics.Shader.Translation.PeriscopeProbe.Enabled ||
Ryujinx.Graphics.Shader.Translation.PeriscopeProbe.Stage2;
public void AddShader(GpuContext context, CachedShaderProgram program, ReadOnlySpan<byte> hostCode, DiskCacheOutputStreams streams = null)
{
// Probe builds never persist: their generated code only matches while the flag is on.
if (_mvppCodegenProbeActive)
{
return;
}
uint stagesBitMask = 0;
for (int index = 0; index < program.Shaders.Length; index++)
{
CachedShaderStage shader = program.Shaders[index];
if (shader == null || (shader.Info != null && shader.Info.Stage == ShaderStage.Compute))
{
continue;
}
stagesBitMask |= 1u << index;
}
FileStream tocFileStream = streams != null ? streams.TocFileStream : DiskCacheCommon.OpenFile(_basePath, SharedTocFileName, writable: true);
FileStream dataFileStream = streams != null ? streams.DataFileStream : DiskCacheCommon.OpenFile(_basePath, SharedDataFileName, writable: true);
ulong timestamp = (ulong)DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds;
if (tocFileStream.Length == 0)
{
TocHeader header = new();
CreateToc(tocFileStream, ref header, TocsMagic, CodeGenVersion, timestamp);
}
tocFileStream.Seek(0, SeekOrigin.End);
dataFileStream.Seek(0, SeekOrigin.End);
BinarySerializer tocWriter = new(tocFileStream);
BinarySerializer dataWriter = new(dataFileStream);
ulong dataOffset = (ulong)dataFileStream.Position;
tocWriter.Write(ref dataOffset);
DataEntry entry = new()
{
StagesBitMask = stagesBitMask,
};
dataWriter.BeginCompression(DiskCacheCommon.GetCompressionAlgorithm());
dataWriter.Write(ref entry);
DataEntryPerStage stageEntry = new();
for (int index = 0; index < program.Shaders.Length; index++)
{
CachedShaderStage shader = program.Shaders[index];
if (shader == null)
{
continue;
}
stageEntry.GuestCodeIndex = _guestStorage.AddShader(shader.Code, shader.Cb1Data);
dataWriter.Write(ref stageEntry);
}
program.SpecializationState.Write(ref dataWriter);
dataWriter.EndCompression();
if (streams == null)
{
tocFileStream.Dispose();
dataFileStream.Dispose();
}
if (hostCode.IsEmpty)
{
return;
}
WriteHostCode(context, hostCode, program.Shaders, streams, timestamp);
}
/// <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 FileStream tocFileStream = DiskCacheCommon.OpenFile(_basePath, SharedTocFileName, writable: true);
using FileStream 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 FileStream tocFileStream = DiskCacheCommon.OpenFile(_basePath, GetHostTocFileName(context), writable: true);
using FileStream dataFileStream = DiskCacheCommon.OpenFile(_basePath, GetHostDataFileName(context), writable: true);
tocFileStream.SetLength(0);
dataFileStream.SetLength(0);
}
/// <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="shaders">Shader stages to be added to the host cache</param>
/// <param name="streams">Output streams to use</param>
/// <param name="timestamp">File creation timestamp</param>
private void WriteHostCode(
GpuContext context,
ReadOnlySpan<byte> hostCode,
CachedShaderStage[] shaders,
DiskCacheOutputStreams streams,
ulong timestamp)
{
FileStream tocFileStream = streams != null ? streams.HostTocFileStream : DiskCacheCommon.OpenFile(_basePath, GetHostTocFileName(context), writable: true);
FileStream dataFileStream = streams != null ? streams.HostDataFileStream : DiskCacheCommon.OpenFile(_basePath, GetHostDataFileName(context), writable: true);
if (tocFileStream.Length == 0)
{
TocHeader header = new();
CreateToc(tocFileStream, ref header, TochMagic, 0, timestamp);
}
tocFileStream.Seek(0, SeekOrigin.End);
dataFileStream.Seek(0, SeekOrigin.End);
BinarySerializer tocWriter = new(tocFileStream);
BinarySerializer dataWriter = new(dataFileStream);
OffsetAndSize offsetAndSize = new()
{
Offset = (ulong)dataFileStream.Position,
UncompressedSize = (uint)hostCode.Length,
};
long dataStartPosition = dataFileStream.Position;
BinarySerializer.WriteCompressed(dataFileStream, hostCode, DiskCacheCommon.GetCompressionAlgorithm());
offsetAndSize.CompressedSize = (uint)(dataFileStream.Position - dataStartPosition);
tocWriter.Write(ref offsetAndSize);
dataWriter.BeginCompression(DiskCacheCommon.GetCompressionAlgorithm());
for (int index = 0; index < shaders.Length; index++)
{
if (shaders[index] != null)
{
WriteShaderProgramInfo(ref dataWriter, shaders[index].Info);
}
}
dataWriter.EndCompression();
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>
/// <param name="timestamp">File creation timestamp</param>
private static void CreateToc(FileStream tocFileStream, ref TocHeader header, uint magic, uint codegenVersion, ulong timestamp)
{
BinarySerializer writer = new(tocFileStream);
header.Magic = magic;
header.FormatVersion = FileFormatVersionPacked;
header.CodeGenVersion = codegenVersion;
header.Padding = 0;
header.Reserved = 0;
header.Timestamp = timestamp;
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();
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.GeometryVerticesPerPrimitive,
dataInfo.GeometryMaxOutputVertices,
dataInfo.ThreadsPerInputPrimitive,
dataInfo.UsesFragCoord,
dataInfo.UsesInstanceId,
dataInfo.UsesDrawParameters,
dataInfo.UsesRtLayer,
// [MVPP CUTOUT PROBE] UsesDiscard is NOT serialized (kept out of the cache format on purpose:
// no version bump, no forced recompile). Disk-cache-loaded shaders read false here; the probe
// is meant to run with the shader cache disabled so every shader is freshly translated.
false,
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()
{
CBuffersCount = (ushort)info.CBuffers.Count,
SBuffersCount = (ushort)info.SBuffers.Count,
TexturesCount = (ushort)info.Textures.Count,
ImagesCount = (ushort)info.Images.Count,
Stage = info.Stage,
GeometryVerticesPerPrimitive = (byte)info.GeometryVerticesPerPrimitive,
GeometryMaxOutputVertices = (ushort)info.GeometryMaxOutputVertices,
ThreadsPerInputPrimitive = (ushort)info.ThreadsPerInputPrimitive,
UsesFragCoord = info.UsesFragCoord,
UsesInstanceId = info.UsesInstanceId,
UsesDrawParameters = info.UsesDrawParameters,
UsesRtLayer = info.UsesRtLayer,
ClipDistancesWritten = info.ClipDistancesWritten,
FragmentOutputMap = info.FragmentOutputMap,
};
dataWriter.WriteWithMagicAndSize(ref dataInfo, ShdiMagic);
for (int index = 0; index < info.CBuffers.Count; index++)
{
BufferDescriptor entry = info.CBuffers[index];
dataWriter.WriteWithMagicAndSize(ref entry, BufdMagic);
}
for (int index = 0; index < info.SBuffers.Count; index++)
{
BufferDescriptor entry = info.SBuffers[index];
dataWriter.WriteWithMagicAndSize(ref entry, BufdMagic);
}
for (int index = 0; index < info.Textures.Count; index++)
{
TextureDescriptor entry = info.Textures[index];
dataWriter.WriteWithMagicAndSize(ref entry, TexdMagic);
}
for (int index = 0; index < info.Images.Count; index++)
{
TextureDescriptor entry = info.Images[index];
dataWriter.WriteWithMagicAndSize(ref entry, TexdMagic);
}
}
}
}