diff --git a/src/Ryujinx.Common/Configuration/TextureFileFormat.cs b/src/Ryujinx.Common/Configuration/TextureFileFormat.cs new file mode 100644 index 000000000..934a50666 --- /dev/null +++ b/src/Ryujinx.Common/Configuration/TextureFileFormat.cs @@ -0,0 +1,12 @@ +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration +{ + [JsonConverter(typeof(TypedStringEnumConverter))] + public enum TextureFileFormat + { + Dds, + Png, + } +} diff --git a/src/Ryujinx.Graphics.Gpu/GpuContext.cs b/src/Ryujinx.Graphics.Gpu/GpuContext.cs index 048d32fb7..b733c2643 100644 --- a/src/Ryujinx.Graphics.Gpu/GpuContext.cs +++ b/src/Ryujinx.Graphics.Gpu/GpuContext.cs @@ -2,6 +2,7 @@ using Ryujinx.Common; using Ryujinx.Graphics.Device; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Gpu.Engine.GPFifo; +using Ryujinx.Graphics.Gpu.Image; using Ryujinx.Graphics.Gpu.Memory; using Ryujinx.Graphics.Gpu.Shader; using Ryujinx.Graphics.Gpu.Synchronization; @@ -45,6 +46,8 @@ namespace Ryujinx.Graphics.Gpu /// public Window Window { get; } + public DiskTextureStorage DiskTextureStorage { get; } + /// /// Internal sequence number, used to avoid needless resource data updates /// in the middle of a command buffer before synchronizations. @@ -123,6 +126,8 @@ namespace Ryujinx.Graphics.Gpu Window = new Window(this); + DiskTextureStorage = new DiskTextureStorage(); + HostInitalized = new ManualResetEvent(false); _gpuReadyEvent = new ManualResetEvent(false); @@ -283,6 +288,8 @@ namespace Ryujinx.Graphics.Gpu physicalMemory.ShaderCache.Initialize(cancellationToken); } + DiskTextureStorage.Initialize(); + _gpuReadyEvent.Set(); } diff --git a/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs b/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs index fbb7399ca..3e4a7e745 100644 --- a/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs +++ b/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs @@ -49,7 +49,7 @@ namespace Ryujinx.Graphics.Gpu /// /// Title id of the current running game. - /// Used by the shader cache. + /// Used by the shader cache and texture dumping. /// public static string TitleId; @@ -72,6 +72,26 @@ namespace Ryujinx.Graphics.Gpu /// Enables or disables color space passthrough, if available. /// public static bool EnableColorSpacePassthrough = false; + + /// + /// Base directory used to write the game textures, if texture dump is enabled. + /// + public static string TextureDumpPath; + + /// + /// Indicates if textures should be saved using the PNG file format. If disabled, textures are saved as DDS. + /// + public static bool TextureDumpFormatPng; + + /// + /// Enables dumping textures to file. + /// + public static bool EnableTextureDump; + + /// + /// Monitors dumped texture files for change and applies them in real-time if enabled. + /// + public static bool EnableTextureRealTimeEdit; } #pragma warning restore CA2211 } diff --git a/src/Ryujinx.Graphics.Gpu/Image/DiskTextureStorage.cs b/src/Ryujinx.Graphics.Gpu/Image/DiskTextureStorage.cs new file mode 100644 index 000000000..79d6a037b --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/DiskTextureStorage.cs @@ -0,0 +1,1141 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Texture; +using Ryujinx.Graphics.Texture.Astc; +using Ryujinx.Graphics.Texture.FileFormats; +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace Ryujinx.Graphics.Gpu.Image +{ + public class DiskTextureStorage + { + private const long MinTimeDeltaForRealTimeLoad = 5; // Seconds. + + private const int StrideAlignment = 4; + + private enum FileFormat + { + Dds, + Png + } + + private struct ScopedLoadLock : IDisposable + { + private readonly DiskTextureStorage _storage; + private readonly string _outputFileName; + private bool _isNewDump; + + public ScopedLoadLock(DiskTextureStorage storage, string outputFileName) + { + _storage = storage; + _outputFileName = outputFileName; + _isNewDump = storage._newDumpFiles.TryAdd(outputFileName, long.MaxValue); + } + + public void Cancel() + { + _isNewDump = false; + _storage._newDumpFiles.TryRemove(_outputFileName, out _); + } + + public readonly void Dispose() + { + if (_isNewDump && TryGetFileTimestamp(_outputFileName, out long timestamp)) + { + _storage._newDumpFiles[_outputFileName] = timestamp; + } + } + } + + private readonly struct TextureRequest + { + public readonly int Width; + public readonly int Height; + public readonly int Depth; + public readonly int Layers; + public readonly int Levels; + public readonly Format Format; + public readonly Target Target; + public readonly byte[] Data; + + public TextureRequest(int width, int height, int depth, int layers, int levels, Format format, Target target, byte[] data) + { + Width = width; + Height = height; + Depth = depth; + Layers = layers; + Levels = levels; + Format = format; + Target = target; + Data = data; + } + } + + private AsyncWorkQueue<(Texture, TextureRequest)> _exportQueue; + private readonly List _importList; + + private readonly ConcurrentDictionary _newDumpFiles; + private readonly ConcurrentDictionary _fileToTextureMap; + private FileSystemWatcher _fileSystemWatcher; + + private string _outputDirectoryPath; + private FileFormat _outputFormat; + private bool _enableTextureDump; + private bool _enableRealTimeTextureEdit; + + internal bool IsActive => !string.IsNullOrEmpty(_outputDirectoryPath) || _importList.Count != 0; + + internal DiskTextureStorage() + { + _importList = new List(); + + _newDumpFiles = new ConcurrentDictionary(); + _fileToTextureMap = new ConcurrentDictionary(); + } + + internal void Initialize() + { + _enableTextureDump = GraphicsConfig.EnableTextureDump; + _enableRealTimeTextureEdit = GraphicsConfig.EnableTextureRealTimeEdit; + + if (_enableRealTimeTextureEdit) + { + _fileSystemWatcher = new FileSystemWatcher(); + _fileSystemWatcher.Changed += OnChanged; + } + + string textureDumpPath = GraphicsConfig.TextureDumpPath; + + if (!string.IsNullOrEmpty(textureDumpPath)) + { + textureDumpPath = Path.Combine(textureDumpPath, GraphicsConfig.TitleId); + } + + SetOutputDirectory(textureDumpPath); + _outputFormat = GraphicsConfig.TextureDumpFormatPng ? FileFormat.Png : FileFormat.Dds; + + if (_enableTextureDump) + { + _exportQueue = new AsyncWorkQueue<(Texture, TextureRequest)>(ExportTexture, "GPU.TextureExportQueue"); + } + } + + private void OnChanged(object sender, FileSystemEventArgs e) + { + if (e.ChangeType != WatcherChangeTypes.Changed) + { + return; + } + + // If this a new file that we just created, ignore it. + if (_newDumpFiles.TryGetValue(e.FullPath, out long savedTimestamp) && + TryGetFileTimestamp(e.FullPath, out long currentTimestamp) && + savedTimestamp > currentTimestamp - MinTimeDeltaForRealTimeLoad) + { + return; + } + + for (int attempt = 0; attempt < 100; attempt++) + { + try + { + File.ReadAllBytes(e.FullPath); + break; + } + catch (Exception) + { + Thread.Sleep(10); + } + } + + if (_fileToTextureMap.TryGetValue(e.Name, out Texture texture)) + { + texture.ForceReimport(); + } + } + + public void AddInputDirectory(string directoryPath) + { + if (Directory.Exists(directoryPath) && !_importList.Contains(directoryPath)) + { + _importList.Add(directoryPath); + } + } + + public void SetOutputDirectory(string directoryPath) + { + string previousOutputDirectoryPath = _outputDirectoryPath; + + if (!string.IsNullOrEmpty(previousOutputDirectoryPath)) + { + _importList.Remove(previousOutputDirectoryPath); + } + + bool hasOutputDir = !string.IsNullOrEmpty(directoryPath); + + if (hasOutputDir) + { + try + { + Directory.CreateDirectory(directoryPath); + } + catch (Exception ex) + { + LogDirCreationException(ex, directoryPath); + hasOutputDir = false; + } + } + + if (hasOutputDir) + { + _outputDirectoryPath = directoryPath; + AddInputDirectory(directoryPath); + + if (_enableRealTimeTextureEdit) + { + _fileSystemWatcher.Path = directoryPath; + _fileSystemWatcher.EnableRaisingEvents = true; + } + } + else + { + _outputDirectoryPath = null; + } + } + + internal TextureInfoOverride? ImportTexture(out MemoryOwner cachedData, Texture texture, byte[] data) + { + cachedData = default; + + if (!IsSupportedFormat(texture.Format)) + { + return null; + } + + TextureInfoOverride? infoOverride = ImportDdsTexture(out cachedData, texture, data); + + if (!infoOverride.HasValue) + { + infoOverride = ImportPngTexture(out cachedData, texture, data); + } + + return infoOverride; + } + + private TextureInfoOverride? ImportDdsTexture(out MemoryOwner cachedData, Texture texture, byte[] data) + { + cachedData = default; + + if (!IsSupportedFormat(texture.Format)) + { + return null; + } + + TextureRequest request = new( + texture.Width, + texture.Height, + texture.Depth, + texture.Layers, + texture.Info.Levels, + texture.Format, + texture.Target, + data); + + ImageParameters parameters = default; + MemoryOwner buffer = null; + + bool imported = false; + string fileName = BuildFileName(request, "dds"); + + foreach (string inputDirectoryPath in _importList) + { + string inputFileName = Path.Combine(inputDirectoryPath, fileName); + + if (File.Exists(inputFileName)) + { + _fileToTextureMap.AddOrUpdate(fileName, texture, (key, old) => texture); + + byte[] imageFile = null; + + try + { + imageFile = File.ReadAllBytes(inputFileName); + } + catch (IOException ex) + { + LogReadException(ex, inputFileName); + break; + } + + ImageLoadResult loadResult = DdsFileFormat.TryLoadHeader(imageFile, out parameters); + + if (loadResult != ImageLoadResult.Success) + { + LogFailureResult(loadResult, inputFileName); + break; + } + + buffer = MemoryOwner.Rent(DdsFileFormat.CalculateSize(parameters)); + + loadResult = DdsFileFormat.TryLoadData(imageFile, buffer.Span); + + if (loadResult != ImageLoadResult.Success) + { + LogFailureResult(loadResult, inputFileName); + break; + } + + imported = true; + break; + } + } + + if (!imported) + { + return null; + } + + if (parameters.Format == ImageFormat.B8G8R8A8Srgb || parameters.Format == ImageFormat.B8G8R8A8Unorm) + { + ConvertBgraToRgbaInPlace(buffer.Span); + } + + cachedData = buffer; + + return new TextureInfoOverride( + parameters.Width, + parameters.Height, + parameters.DepthOrLayers, + parameters.Levels, + ConvertToFormat(parameters.Format)); + } + + private TextureInfoOverride? ImportPngTexture(out MemoryOwner cachedData, Texture texture, byte[] data) + { + cachedData = default; + + if (!IsSupportedFormat(texture.Format)) + { + return null; + } + + TextureRequest request = new( + texture.Width, + texture.Height, + texture.Depth, + texture.Layers, + texture.Info.Levels, + texture.Format, + texture.Target, + data); + + MemoryOwner buffer = null; + + int importedFirstLevel = 0; + int importedWidth = 0; + int importedHeight = 0; + int levels = 0; + int slices = 0; + int writtenSize = 0; + int offset = 0; + + DoForEachSlice(request, (level, slice, _, _) => + { + int sliceSize = (importedWidth | importedHeight) != 0 ? Math.Max(1, importedWidth >> level) * Math.Max(1, importedHeight >> level) * 4 : 0; + + bool imported = false; + string fileName = BuildFileName(request, level, slice, "png"); + + foreach (string inputDirectoryPath in _importList) + { + string inputFileName = Path.Combine(inputDirectoryPath, fileName); + + if (File.Exists(inputFileName)) + { + _fileToTextureMap.AddOrUpdate(fileName, texture, (key, old) => texture); + + byte[] imageFile = null; + + try + { + imageFile = File.ReadAllBytes(inputFileName); + } + catch (IOException ex) + { + LogReadException(ex, inputFileName); + break; + } + + ImageLoadResult loadResult = PngFileFormat.TryLoadHeader(imageFile, out ImageParameters parameters); + + if (loadResult != ImageLoadResult.Success) + { + LogFailureResult(loadResult, inputFileName); + break; + } + + int importedSizeWL = Math.Max(1, importedWidth >> level); + int importedSizeHL = Math.Max(1, importedHeight >> level); + + if (writtenSize == 0 || (importedSizeWL == parameters.Width && importedSizeHL == parameters.Height)) + { + if (writtenSize == 0) + { + importedFirstLevel = level; + importedWidth = parameters.Width << level; + importedHeight = parameters.Height << level; + sliceSize = Math.Max(1, importedWidth >> level) * Math.Max(1, importedHeight >> level) * 4; + buffer = MemoryOwner.Rent(CalculateSize(importedWidth, importedHeight, request.Depth, request.Layers, request.Levels)); + } + + loadResult = PngFileFormat.TryLoadData(imageFile, buffer.Span.Slice(offset, sliceSize)); + + if (loadResult != ImageLoadResult.Success) + { + LogFailureResult(loadResult, inputFileName); + break; + } + } + else + { + break; + } + + imported = true; + break; + } + } + + if (imported) + { + levels = level + 1; + + if (level == importedFirstLevel) + { + slices = slice + 1; + } + + writtenSize = offset + sliceSize; + } + + offset += sliceSize; + + return imported; + }); + + if (writtenSize == 0) + { + return null; + } + + if (writtenSize == buffer.Length) + { + cachedData = buffer; + } + else + { + using (buffer) + { + cachedData = MemoryOwner.RentCopy(buffer.Span[..writtenSize]); + } + } + + Format format; + + if (IsSupportedSnormFormat(request.Format)) + { + format = Format.R8G8B8A8Snorm; + } + else if (IsSupportedSrgbFormat(request.Format)) + { + format = Format.R8G8B8A8Srgb; + } + else + { + format = Format.R8G8B8A8Unorm; + } + + return new TextureInfoOverride( + importedWidth, + importedHeight, + slices, + levels, + new FormatInfo(format, 1, 1, 4, 4)); + } + + private static int CalculateSize(int width, int height, int depth, int layers, int levels) + { + int size = 0; + + for (int level = 0; level < levels; level++) + { + int w = Math.Max(1, width >> level); + int h = Math.Max(1, height >> level); + int d = Math.Max(1, depth >> level); + int sliceSize = w * h * 4; + + size += sliceSize * layers * d; + } + + return size; + } + + internal void EnqueueTextureDataForExport(Texture texture, byte[] data) + { + if (_enableTextureDump && !string.IsNullOrEmpty(_outputDirectoryPath)) + { + _exportQueue.Add((texture, new( + texture.Width, + texture.Height, + texture.Depth, + texture.Layers, + texture.Info.Levels, + texture.Format, + texture.Target, + data))); + } + } + + private void ExportTexture((Texture, TextureRequest) tuple) + { + if (_outputFormat == FileFormat.Png) + { + ExportPngTexturePerSlice(tuple.Item1, tuple.Item2); + } + else + { + ExportDdsTexture(tuple.Item1, tuple.Item2); + } + } + + private void ExportDdsTexture(Texture texture, TextureRequest request) + { + if (!TryGetDimensions(request.Target, out ImageDimensions imageDimensions)) + { + return; + } + + MemoryOwner dataOwner = null; + ReadOnlySpan data = request.Data; + + ImageFormat imageFormat = GetFormat(request.Format); + + if (imageFormat == ImageFormat.Unknown) + { + dataOwner = ConvertFormatToRgba8(request); + if (dataOwner == null) + { + return; + } + + data = dataOwner.Span; + imageFormat = IsSupportedSrgbFormat(request.Format) ? ImageFormat.R8G8B8A8Srgb : ImageFormat.R8G8B8A8Unorm; + } + + string fileName = BuildFileName(request, "dds"); + string outputFileName = Path.Combine(_outputDirectoryPath, fileName); + + using ScopedLoadLock loadLock = new(this, outputFileName); + + _fileToTextureMap.TryAdd(fileName, texture); + + ImageParameters parameters = new( + request.Width, + request.Height, + request.Depth * request.Layers, + request.Levels, + imageFormat, + imageDimensions); + + try + { + using FileStream fs = new(outputFileName, FileMode.Create); + DdsFileFormat.Save(fs, parameters, data); + } + catch (IOException ex) + { + LogWriteException(ex, outputFileName); + loadLock.Cancel(); + } + finally + { + dataOwner?.Dispose(); + } + } + + private void ExportPngTexturePerSlice(Texture texture, TextureRequest request) + { + using MemoryOwner data = ConvertFormatToRgba8(request); + if (data == null) + { + return; + } + + DoForEachSlice(request, (level, slice, offset, sliceSize) => + { + ReadOnlySpan buffer = data.Span; + + int w = Math.Max(1, request.Width >> level); + int h = Math.Max(1, request.Height >> level); + + string fileName = BuildFileName(request, level, slice, "png"); + string outputFileName = Path.Combine(_outputDirectoryPath, fileName); + + using ScopedLoadLock loadLock = new(this, outputFileName); + + _fileToTextureMap.TryAdd(fileName, texture); + + ImageParameters parameters = new(w, h, 1, 1, ImageFormat.R8G8B8A8Unorm, ImageDimensions.Dim2D); + + try + { + using FileStream fs = new(outputFileName, FileMode.Create); + PngFileFormat.Save(fs, parameters, buffer.Slice(offset, sliceSize), fastMode: true); + } + catch (IOException ex) + { + LogWriteException(ex, outputFileName); + loadLock.Cancel(); + return false; + } + + return true; + }); + } + + private static bool TryGetFileTimestamp(string fileName, out long timestamp) + { + try + { + DateTime time = File.GetLastWriteTimeUtc(fileName); + timestamp = ((DateTimeOffset)time).ToUnixTimeSeconds(); + return true; + } + catch (Exception) + { + timestamp = 0; + return false; + } + } + + private static void DoForEachSlice(in TextureRequest request, Func callback) + { + bool is3D = request.Depth > 1; + int offset = 0; + + for (int level = 0; level < request.Levels; level++) + { + int w = Math.Max(1, request.Width >> level); + int h = Math.Max(1, request.Height >> level); + int d = is3D ? Math.Max(1, request.Depth >> level) : request.Layers; + int sliceSize = w * h * 4; + + for (int slice = 0; slice < d; slice++) + { + if (!callback(level, slice, offset, sliceSize)) + { + break; + } + + offset += sliceSize; + } + } + } + + private static string BuildFileName(TextureRequest request, string extension) + { + int w = request.Width; + int h = request.Height; + int d = request.Depth * request.Layers; + string hash = ComputeHash(request.Data); + return $"{GetNamePrefix(request.Target)}_{hash}_{w}x{h}x{d}.{extension}"; + } + + private static string BuildFileName(TextureRequest request, int level, int slice, string extension) + { + int w = request.Width; + int h = request.Height; + int d = request.Depth * request.Layers; + string hash = ComputeHash(request.Data); + return $"{GetNamePrefix(request.Target)}_{hash}_{w}x{h}x{d}_{level}x{slice}.{extension}"; + } + + private static string GetNamePrefix(Target target) + { + return target switch + { + Target.Texture2D => "tex2d", + Target.Texture2DArray => "texa2d", + Target.Texture3D => "tex3d", + Target.Cubemap => "texcube", + Target.CubemapArray => "texacube", + _ => "tex", + }; + } + + private static string ComputeHash(byte[] data) + { + Hash128 hash = XXHash128.ComputeHash(data); + return $"{hash.High:x16}{hash.Low:x16}"; + } + + private static ImageFormat GetFormat(Format format) + { + return format switch + { + Format.Bc1RgbaSrgb => ImageFormat.Bc1RgbaSrgb, + Format.Bc1RgbaUnorm => ImageFormat.Bc1RgbaUnorm, + Format.Bc2Srgb => ImageFormat.Bc2Srgb, + Format.Bc2Unorm => ImageFormat.Bc2Unorm, + Format.Bc3Srgb => ImageFormat.Bc3Srgb, + Format.Bc3Unorm => ImageFormat.Bc3Unorm, + Format.Bc4Snorm => ImageFormat.Bc4Snorm, + Format.Bc4Unorm => ImageFormat.Bc4Unorm, + Format.Bc5Snorm => ImageFormat.Bc5Snorm, + Format.Bc5Unorm => ImageFormat.Bc5Unorm, + Format.Bc7Srgb => ImageFormat.Bc7Srgb, + Format.Bc7Unorm => ImageFormat.Bc7Unorm, + Format.R8Unorm => ImageFormat.R8Unorm, + Format.R8G8Unorm => ImageFormat.R8G8Unorm, + Format.R8G8B8A8Srgb => ImageFormat.R8G8B8A8Srgb, + Format.R8G8B8A8Unorm => ImageFormat.R8G8B8A8Unorm, + Format.R5G6B5Unorm => ImageFormat.R5G6B5Unorm, + Format.R5G5B5A1Unorm => ImageFormat.R5G5B5A1Unorm, + Format.R4G4B4A4Unorm => ImageFormat.R4G4B4A4Unorm, + _ => ImageFormat.Unknown, + }; + } + + private static bool TryGetDimensions(Target target, out ImageDimensions imageDimensions) + { + switch (target) + { + case Target.Texture2D: + imageDimensions = ImageDimensions.Dim2D; + return true; + case Target.Texture2DArray: + imageDimensions = ImageDimensions.Dim2DArray; + return true; + case Target.Texture3D: + imageDimensions = ImageDimensions.Dim3D; + return true; + case Target.Cubemap: + imageDimensions = ImageDimensions.DimCube; + return true; + case Target.CubemapArray: + imageDimensions = ImageDimensions.DimCubeArray; + return true; + } + + imageDimensions = default; + return false; + } + + private static MemoryOwner ConvertFormatToRgba8(in TextureRequest request) + { + byte[] data = request.Data; + int width = request.Width; + int height = request.Height; + int depth = request.Depth; + int layers = request.Layers; + int levels = request.Levels; + + switch (request.Format) + { + case Format.Astc4x4Srgb: + case Format.Astc4x4Unorm: + return DecodeAstc(data, 4, 4, width, height, depth, levels, layers); + case Format.Astc5x4Srgb: + case Format.Astc5x4Unorm: + return DecodeAstc(data, 5, 4, width, height, depth, levels, layers); + case Format.Astc5x5Srgb: + case Format.Astc5x5Unorm: + return DecodeAstc(data, 5, 5, width, height, depth, levels, layers); + case Format.Astc6x5Srgb: + case Format.Astc6x5Unorm: + return DecodeAstc(data, 6, 5, width, height, depth, levels, layers); + case Format.Astc6x6Srgb: + case Format.Astc6x6Unorm: + return DecodeAstc(data, 6, 6, width, height, depth, levels, layers); + case Format.Astc8x5Srgb: + case Format.Astc8x5Unorm: + return DecodeAstc(data, 8, 5, width, height, depth, levels, layers); + case Format.Astc8x6Srgb: + case Format.Astc8x6Unorm: + return DecodeAstc(data, 8, 6, width, height, depth, levels, layers); + case Format.Astc8x8Srgb: + case Format.Astc8x8Unorm: + return DecodeAstc(data, 8, 8, width, height, depth, levels, layers); + case Format.Astc10x5Srgb: + case Format.Astc10x5Unorm: + return DecodeAstc(data, 10, 5, width, height, depth, levels, layers); + case Format.Astc10x6Srgb: + case Format.Astc10x6Unorm: + return DecodeAstc(data, 10, 6, width, height, depth, levels, layers); + case Format.Astc10x8Srgb: + case Format.Astc10x8Unorm: + return DecodeAstc(data, 10, 8, width, height, depth, levels, layers); + case Format.Astc10x10Srgb: + case Format.Astc10x10Unorm: + return DecodeAstc(data, 10, 10, width, height, depth, levels, layers); + case Format.Astc12x10Srgb: + case Format.Astc12x10Unorm: + return DecodeAstc(data, 12, 10, width, height, depth, levels, layers); + case Format.Astc12x12Srgb: + case Format.Astc12x12Unorm: + return DecodeAstc(data, 12, 12, width, height, depth, levels, layers); + case Format.Bc1RgbaSrgb: + case Format.Bc1RgbaUnorm: + return BCnDecoder.DecodeBC1(data, width, height, depth, levels, layers); + case Format.Bc2Srgb: + case Format.Bc2Unorm: + return BCnDecoder.DecodeBC2(data, width, height, depth, levels, layers); + case Format.Bc3Srgb: + case Format.Bc3Unorm: + return BCnDecoder.DecodeBC3(data, width, height, depth, levels, layers); + case Format.Bc4Snorm: + case Format.Bc4Unorm: + using (MemoryOwner decoded = BCnDecoder.DecodeBC4(data, width, height, depth, levels, layers, request.Format == Format.Bc4Snorm)) + { + return ConvertRToRgba(decoded.Span, request); + } + case Format.Bc5Snorm: + case Format.Bc5Unorm: + using (MemoryOwner decoded = BCnDecoder.DecodeBC5(data, width, height, depth, levels, layers, request.Format == Format.Bc5Snorm)) + { + return ConvertRgToRgba(decoded.Span, request); + } + case Format.Bc7Srgb: + case Format.Bc7Unorm: + return BCnDecoder.DecodeBC7(data, width, height, depth, levels, layers); + case Format.Etc2RgbaSrgb: + case Format.Etc2RgbaUnorm: + return ETC2Decoder.DecodeRgba(data, width, height, depth, levels, layers); + case Format.Etc2RgbSrgb: + case Format.Etc2RgbUnorm: + return ETC2Decoder.DecodeRgb(data, width, height, depth, levels, layers); + case Format.Etc2RgbPtaSrgb: + case Format.Etc2RgbPtaUnorm: + return ETC2Decoder.DecodePta(data, width, height, depth, levels, layers); + case Format.R8Unorm: + return ConvertRToRgba(request.Data, request); + case Format.R8G8Unorm: + return ConvertRgToRgba(request.Data, request); + case Format.R8G8B8A8Unorm: + case Format.R8G8B8A8Srgb: + return MemoryOwner.RentCopy(request.Data); + case Format.B5G6R5Unorm: + case Format.R5G6B5Unorm: + return PixelConverter.ConvertR5G6B5ToR8G8B8A8(data, width); + case Format.B5G5R5A1Unorm: + case Format.R5G5B5X1Unorm: + case Format.R5G5B5A1Unorm: + return PixelConverter.ConvertR5G5B5ToR8G8B8A8(data, width, request.Format == Format.R5G5B5X1Unorm); + case Format.A1B5G5R5Unorm: + return PixelConverter.ConvertA1B5G5R5ToR8G8B8A8(data, width); + case Format.R4G4B4A4Unorm: + return PixelConverter.ConvertR4G4B4A4ToR8G8B8A8(data, width); + } + + return null; + } + + private static MemoryOwner DecodeAstc(byte[] data, int blockWidth, int blockHeight, int width, int height, int depth, int levels, int layers) + { + AstcDecoder.TryDecodeToRgba8P( + data, + blockWidth, + blockHeight, + width, + height, + depth, + levels, + layers, + out MemoryOwner decoded); + + return decoded; + } + + private static void ConvertBgraToRgbaInPlace(Span buffer) + { + for (int i = 0; i < buffer.Length; i += 4) + { + (buffer[i + 2], buffer[i]) = (buffer[i], buffer[i + 2]); + } + } + + private static MemoryOwner ConvertRToRgba(ReadOnlySpan input, in TextureRequest request) + { + MemoryOwner output = MemoryOwner.Rent(CalculateSize(request, 4)); + + int srcBaseOffset = 0; + int dstBaseOffset = 0; + + for (int l = 0; l < request.Levels; l++) + { + int w = Math.Max(1, request.Width >> l); + int h = Math.Max(1, request.Height >> l); + int d = Math.Max(1, request.Depth >> l) * request.Layers; + + int stride = w; + int strideAligned = (stride + StrideAlignment - 1) & -StrideAlignment; + int rows = h * d; + + for (int i = 0; i < rows; i++) + { + int dstOffset = dstBaseOffset + i * stride * 4; + int srcOffset = srcBaseOffset + i * strideAligned; + + for (int j = 0; j < stride; j++) + { + output.Span[dstOffset + j * 4] = input[srcOffset + j]; + output.Span[dstOffset + j * 4 + 3] = 0xff; + } + } + + dstBaseOffset += rows * stride * 4; + srcBaseOffset += rows * strideAligned; + } + + return output; + } + + private static MemoryOwner ConvertRgToRgba(ReadOnlySpan input, in TextureRequest request) + { + MemoryOwner output = MemoryOwner.Rent(CalculateSize(request, 4)); + + int srcBaseOffset = 0; + int dstBaseOffset = 0; + + for (int l = 0; l < request.Levels; l++) + { + int w = Math.Max(1, request.Width >> l); + int h = Math.Max(1, request.Height >> l); + int d = Math.Max(1, request.Depth >> l) * request.Layers; + + int stride = w * 2; + int strideAligned = (stride + StrideAlignment - 1) & -StrideAlignment; + int rows = h * d; + + for (int i = 0; i < rows; i++) + { + int dstOffset = dstBaseOffset + i * stride * 2; + int srcOffset = srcBaseOffset + i * strideAligned; + + for (int j = 0; j < stride; j += 2) + { + output.Span[dstOffset + j * 2] = input[srcOffset + j]; + output.Span[dstOffset + j * 2 + 1] = input[srcOffset + j + 1]; + output.Span[dstOffset + j * 2 + 3] = 0xff; + } + } + + dstBaseOffset += rows * stride * 2; + srcBaseOffset += rows * strideAligned; + } + + return output; + } + + private static int CalculateSize(in TextureRequest request, int bpp) + { + int size = 0; + + for (int l = 0; l < request.Levels; l++) + { + int w = Math.Max(1, request.Width >> l); + int h = Math.Max(1, request.Height >> l); + int d = Math.Max(1, request.Depth >> l) * request.Layers; + + size += w * h * d; + } + + return size * bpp; + } + + private static bool IsSupportedFormat(Format format) + { + switch (format) + { + case Format.Astc4x4Srgb: + case Format.Astc4x4Unorm: + case Format.Astc5x4Srgb: + case Format.Astc5x4Unorm: + case Format.Astc5x5Srgb: + case Format.Astc5x5Unorm: + case Format.Astc6x5Srgb: + case Format.Astc6x5Unorm: + case Format.Astc6x6Srgb: + case Format.Astc6x6Unorm: + case Format.Astc8x5Srgb: + case Format.Astc8x5Unorm: + case Format.Astc8x6Srgb: + case Format.Astc8x6Unorm: + case Format.Astc8x8Srgb: + case Format.Astc8x8Unorm: + case Format.Astc10x5Srgb: + case Format.Astc10x5Unorm: + case Format.Astc10x6Srgb: + case Format.Astc10x6Unorm: + case Format.Astc10x8Srgb: + case Format.Astc10x8Unorm: + case Format.Astc10x10Srgb: + case Format.Astc10x10Unorm: + case Format.Astc12x10Srgb: + case Format.Astc12x10Unorm: + case Format.Astc12x12Srgb: + case Format.Astc12x12Unorm: + case Format.Bc1RgbaSrgb: + case Format.Bc1RgbaUnorm: + case Format.Bc2Srgb: + case Format.Bc2Unorm: + case Format.Bc3Srgb: + case Format.Bc3Unorm: + case Format.Bc4Unorm: + case Format.Bc5Snorm: + case Format.Bc5Unorm: + case Format.Bc7Srgb: + case Format.Bc7Unorm: + case Format.Etc2RgbaSrgb: + case Format.Etc2RgbaUnorm: + case Format.Etc2RgbSrgb: + case Format.Etc2RgbUnorm: + case Format.Etc2RgbPtaSrgb: + case Format.Etc2RgbPtaUnorm: + case Format.R8Unorm: + case Format.R8G8Unorm: + case Format.R8G8B8A8Unorm: + case Format.R8G8B8A8Srgb: + case Format.B5G6R5Unorm: + case Format.R5G6B5Unorm: + case Format.B5G5R5A1Unorm: + case Format.R5G5B5X1Unorm: + case Format.R5G5B5A1Unorm: + case Format.A1B5G5R5Unorm: + case Format.R4G4B4A4Unorm: + return true; + } + + return false; + } + + private static bool IsSupportedSrgbFormat(Format format) + { + switch (format) + { + case Format.Astc4x4Srgb: + case Format.Astc5x4Srgb: + case Format.Astc5x5Srgb: + case Format.Astc6x5Srgb: + case Format.Astc6x6Srgb: + case Format.Astc8x5Srgb: + case Format.Astc8x6Srgb: + case Format.Astc8x8Srgb: + case Format.Astc10x5Srgb: + case Format.Astc10x6Srgb: + case Format.Astc10x8Srgb: + case Format.Astc10x10Srgb: + case Format.Astc12x10Srgb: + case Format.Astc12x12Srgb: + case Format.Bc1RgbaSrgb: + case Format.Bc2Srgb: + case Format.Bc3Srgb: + case Format.Bc7Srgb: + case Format.Etc2RgbaSrgb: + case Format.Etc2RgbSrgb: + case Format.Etc2RgbPtaSrgb: + case Format.R8G8B8A8Srgb: + return true; + } + + return false; + } + + private static bool IsSupportedSnormFormat(Format format) + { + return format == Format.Bc5Snorm; + } + + private static FormatInfo ConvertToFormat(ImageFormat format) + { + return format switch + { + ImageFormat.Bc1RgbaSrgb => new FormatInfo(Format.Bc1RgbaSrgb, 4, 4, 8, 4), + ImageFormat.Bc1RgbaUnorm => new FormatInfo(Format.Bc1RgbaUnorm, 4, 4, 8, 4), + ImageFormat.Bc2Srgb => new FormatInfo(Format.Bc2Srgb, 4, 4, 16, 4), + ImageFormat.Bc2Unorm => new FormatInfo(Format.Bc2Unorm, 4, 4, 16, 4), + ImageFormat.Bc3Srgb => new FormatInfo(Format.Bc3Srgb, 4, 4, 16, 4), + ImageFormat.Bc3Unorm => new FormatInfo(Format.Bc3Unorm, 4, 4, 16, 4), + ImageFormat.Bc4Snorm => new FormatInfo(Format.Bc4Snorm, 4, 4, 8, 1), + ImageFormat.Bc4Unorm => new FormatInfo(Format.Bc4Unorm, 4, 4, 8, 1), + ImageFormat.Bc5Snorm => new FormatInfo(Format.Bc5Snorm, 4, 4, 16, 2), + ImageFormat.Bc5Unorm => new FormatInfo(Format.Bc5Unorm, 4, 4, 16, 2), + ImageFormat.Bc7Srgb => new FormatInfo(Format.Bc7Srgb, 4, 4, 16, 4), + ImageFormat.Bc7Unorm => new FormatInfo(Format.Bc7Unorm, 4, 4, 16, 4), + ImageFormat.R8Unorm => new FormatInfo(Format.R8Unorm, 1, 1, 1, 1), + ImageFormat.R8G8Unorm => new FormatInfo(Format.R8G8Unorm, 1, 1, 2, 2), + ImageFormat.R8G8B8A8Srgb or ImageFormat.B8G8R8A8Srgb => new FormatInfo(Format.R8G8B8A8Srgb, 1, 1, 4, 4), + ImageFormat.R8G8B8A8Unorm or ImageFormat.B8G8R8A8Unorm => new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4), + ImageFormat.R5G6B5Unorm => new FormatInfo(Format.R5G6B5Unorm, 1, 1, 2, 3), + ImageFormat.R5G5B5A1Unorm => new FormatInfo(Format.R5G5B5A1Unorm, 1, 1, 2, 4), + ImageFormat.R4G4B4A4Unorm => new FormatInfo(Format.R4G4B4A4Unorm, 1, 1, 2, 4), + _ => throw new ArgumentException($"Invalid format {format}."), + }; + } + + private static void LogFailureResult(ImageLoadResult result, string fullPath) + { + string fileName = Path.GetFileName(fullPath); + + switch (result) + { + case ImageLoadResult.CorruptedHeader: + Logger.Error?.Print(LogClass.Gpu, $"Failed to load \"{fileName}\" because the file header is corrupted."); + break; + case ImageLoadResult.CorruptedData: + Logger.Error?.Print(LogClass.Gpu, $"Failed to load \"{fileName}\" because the file data is corrupted."); + break; + case ImageLoadResult.DataTooShort: + Logger.Error?.Print(LogClass.Gpu, $"Failed to load \"{fileName}\" because some data is missing from the file."); + break; + case ImageLoadResult.OutputTooShort: + Logger.Error?.Print(LogClass.Gpu, $"Failed to load \"{fileName}\" because the output buffer was not large enough."); + break; + case ImageLoadResult.UnsupportedFormat: + Logger.Error?.Print(LogClass.Gpu, $"Failed to load \"{fileName}\" because the image format is not currently supported."); + break; + } + } + + private static void LogReadException(IOException exception, string fullPath) + { + Logger.Error?.Print(LogClass.Gpu, exception.ToString()); + + string fileName = Path.GetFileName(fullPath); + + Logger.Error?.Print(LogClass.Gpu, $"Failed to load \"{fileName}\", see logged exception for details."); + } + + private static void LogWriteException(IOException exception, string fullPath) + { + Logger.Error?.Print(LogClass.Gpu, exception.ToString()); + + string fileName = Path.GetFileName(fullPath); + + Logger.Error?.Print(LogClass.Gpu, $"Failed to save \"{fileName}\", see logged exception for details."); + } + + private static void LogDirCreationException(Exception exception, string fullPath) + { + Logger.Error?.Print(LogClass.Gpu, exception.ToString()); + Logger.Error?.Print(LogClass.Gpu, $"Failed to create a directory on path \"{fullPath}\", see logged exception for details."); + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs index 3b6c407cc..c379af437 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -65,6 +65,16 @@ namespace Ryujinx.Graphics.Gpu.Image /// public int Height { get; private set; } + /// + /// Texture depth, or 1 if the texture is not a 3D texture. + /// + public int Depth { get; private set; } + + /// + /// Numer of texture layers, or 1 if the texture is not a array texture. + /// + public int Layers { get; private set; } + /// /// Texture information. /// @@ -107,11 +117,13 @@ namespace Ryujinx.Graphics.Gpu.Image /// public int InvalidatedSequence { get; private set; } - private int _depth; - private int _layers; public int FirstLayer { get; private set; } public int FirstLevel { get; private set; } + private TextureInfoOverride? _importOverride; + private bool _forceReimport; + private readonly bool _forRender; + private bool _hasData; private bool _dirty = true; private int _updateCount; @@ -190,6 +202,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// The first mipmap level of the texture, or 0 if the texture has no parent /// The floating point scale factor to initialize with /// The scale mode to initialize with + /// Indicates that the texture will be modified by a draw or blit operation private Texture( GpuContext context, PhysicalMemory physicalMemory, @@ -199,7 +212,8 @@ namespace Ryujinx.Graphics.Gpu.Image int firstLayer, int firstLevel, float scaleFactor, - TextureScaleMode scaleMode) + TextureScaleMode scaleMode, + bool forRender) { InitializeTexture(context, physicalMemory, info, sizeInfo, range); @@ -209,6 +223,8 @@ namespace Ryujinx.Graphics.Gpu.Image ScaleFactor = scaleFactor; ScaleMode = scaleMode; + _forRender = forRender; + InitializeData(true); } @@ -221,17 +237,21 @@ namespace Ryujinx.Graphics.Gpu.Image /// Size information of the texture /// Physical memory ranges where the texture data is located /// The scale mode to initialize with. If scaled, the texture's data is loaded immediately and scaled up + /// Indicates that the texture will be modified by a draw or blit operation public Texture( GpuContext context, PhysicalMemory physicalMemory, TextureInfo info, SizeInfo sizeInfo, MultiRange range, - TextureScaleMode scaleMode) + TextureScaleMode scaleMode, + bool forRender) { ScaleFactor = 1f; // Texture is first loaded at scale 1x. ScaleMode = scaleMode; + _forRender = forRender; + InitializeTexture(context, physicalMemory, info, sizeInfo, range); } @@ -279,7 +299,7 @@ namespace Ryujinx.Graphics.Gpu.Image { Debug.Assert(!isView); - TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, ScaleFactor); + TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, ScaleFactor, _importOverride); HostTexture = _context.Renderer.CreateTexture(createInfo); SynchronizeMemory(); // Load the data. @@ -303,7 +323,7 @@ namespace Ryujinx.Graphics.Gpu.Image ScaleFactor = GraphicsConfig.ResScale; } - TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, ScaleFactor); + TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, ScaleFactor, _importOverride); HostTexture = _context.Renderer.CreateTexture(createInfo); } } @@ -345,9 +365,10 @@ namespace Ryujinx.Graphics.Gpu.Image FirstLayer + firstLayer, FirstLevel + firstLevel, ScaleFactor, - ScaleMode); + ScaleMode, + _forRender); - TextureCreateInfo createInfo = TextureCache.GetCreateInfo(info, _context.Capabilities, ScaleFactor); + TextureCreateInfo createInfo = TextureCache.GetCreateInfo(info, _context.Capabilities, ScaleFactor, null); texture.HostTexture = HostTexture.CreateView(createInfo, firstLayer, firstLevel); _viewStorage.AddView(texture); @@ -491,7 +512,7 @@ namespace Ryujinx.Graphics.Gpu.Image { if (storage == null) { - TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, scale); + TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, scale, _importOverride); storage = _context.Renderer.CreateTexture(createInfo); } @@ -542,7 +563,7 @@ namespace Ryujinx.Graphics.Gpu.Image Logger.Debug?.Print(LogClass.Gpu, $" Recreating view {Info.Width}x{Info.Height} {Info.FormatInfo.Format}."); view.ScaleFactor = scale; - TextureCreateInfo viewCreateInfo = TextureCache.GetCreateInfo(view.Info, _context.Capabilities, scale); + TextureCreateInfo viewCreateInfo = TextureCache.GetCreateInfo(view.Info, _context.Capabilities, scale, _importOverride); ITexture newView = HostTexture.CreateView(viewCreateInfo, view.FirstLayer - FirstLayer, view.FirstLevel - FirstLevel); view.ReplaceStorage(newView); @@ -572,6 +593,11 @@ namespace Ryujinx.Graphics.Gpu.Image return Group.CheckDirty(this, consume); } + public void ForceReimport() + { + _forceReimport = true; + } + /// /// Discards all data for this texture. /// This clears all dirty flags and pending copies from other textures. @@ -598,6 +624,15 @@ namespace Ryujinx.Graphics.Gpu.Image return; } + if (_forceReimport) + { + SynchronizeFull(); + + _forceReimport = false; + + return; + } + if (!_dirty) { return; @@ -644,7 +679,7 @@ namespace Ryujinx.Graphics.Gpu.Image // The decompression is slow, so we want to avoid it as much as possible. // This does a byte-by-byte check and skips the update if the data is equal in this case. // This improves the speed on applications that overwrites ASTC data without changing anything. - if (Info.FormatInfo.Format.IsAstc() && !_context.Capabilities.SupportsAstcCompression) + if (Info.FormatInfo.Format.IsAstc() && !_context.Capabilities.SupportsAstcCompression && !_forceReimport) { if (_updateCount < ByteComparisonSwitchThreshold) { @@ -689,6 +724,11 @@ namespace Ryujinx.Graphics.Gpu.Image { BlacklistScale(); + if (HasImportOverride()) + { + return; + } + Group.CheckDirty(this, true); AlwaysFlushOnOverlap = true; @@ -726,6 +766,11 @@ namespace Ryujinx.Graphics.Gpu.Image { BlacklistScale(); + if (HasImportOverride()) + { + return; + } + HostTexture.SetData(data, layer, level, region); _currentData = null; @@ -745,8 +790,8 @@ namespace Ryujinx.Graphics.Gpu.Image int width = Info.Width; int height = Info.Height; - int depth = _depth; - int layers = single ? 1 : _layers; + int depth = Depth; + int layers = single ? 1 : Layers; int levels = single ? 1 : (Info.Levels - level); width = Math.Max(width >> level, 1); @@ -789,11 +834,55 @@ namespace Ryujinx.Graphics.Gpu.Image } IMemoryOwner result = linear; + FormatInfo formatInfo = Info.FormatInfo; + + if (_context.DiskTextureStorage.IsActive && !_forRender) + { + TextureInfoOverride? importOverride = _context.DiskTextureStorage.ImportTexture(out var importedTexture, this, result.Memory.ToArray()); + + if (importOverride.HasValue) + { + if (!_importOverride.HasValue || !_importOverride.Equals(importOverride)) + { + bool hadImportOverride = HasImportOverride(); + + _importOverride = importOverride; + + if (hadImportOverride || HasImportOverride()) + { + InvalidatedSequence++; + TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, ScaleFactor, importOverride); + ReplaceStorage(_context.Renderer.CreateTexture(createInfo)); + + if (_viewStorage != this) + { + _viewStorage.RemoveView(this); + } + } + } + + TextureInfoOverride infoOverride = importOverride.Value; + + width = infoOverride.Width; + height = infoOverride.Height; + sliceDepth = Target == Target.Texture3D ? infoOverride.DepthOrLayers : 1; + layers = Target != Target.Texture3D ? Info.DepthOrLayers : 1; + levels = infoOverride.Levels; + formatInfo = infoOverride.FormatInfo; + + result.Dispose(); + result = importedTexture; + } + else if (!_hasData) + { + _context.DiskTextureStorage.EnqueueTextureDataForExport(this, result.Memory.ToArray()); + } + } // Handle compressed cases not supported by the host: // - ASTC is usually not supported on desktop cards. // - BC4/BC5 is not supported on 3D textures. - if (!_context.Capabilities.SupportsAstcCompression && Format.IsAstc()) + if (!_context.Capabilities.SupportsAstcCompression && formatInfo.Format.IsAstc()) { using (result) { @@ -824,9 +913,9 @@ namespace Ryujinx.Graphics.Gpu.Image return decoded; } } - else if (!_context.Capabilities.SupportsEtc2Compression && Format.IsEtc2()) + else if (!_context.Capabilities.SupportsEtc2Compression && formatInfo.Format.IsEtc2()) { - switch (Format) + switch (formatInfo.Format) { case Format.Etc2RgbaSrgb: case Format.Etc2RgbaUnorm: @@ -848,9 +937,9 @@ namespace Ryujinx.Graphics.Gpu.Image } } } - else if (!TextureCompatibility.HostSupportsBcFormat(Format, Target, _context.Capabilities)) + else if (!TextureCompatibility.HostSupportsBcFormat(formatInfo.Format, Target, _context.Capabilities)) { - switch (Format) + switch (formatInfo.Format) { case Format.Bc1RgbaSrgb: case Format.Bc1RgbaUnorm: @@ -896,7 +985,7 @@ namespace Ryujinx.Graphics.Gpu.Image } } } - else if (!_context.Capabilities.SupportsR4G4Format && Format == Format.R4G4Unorm) + else if (!_context.Capabilities.SupportsR4G4Format && formatInfo.Format == Format.R4G4Unorm) { using (result) { @@ -915,7 +1004,7 @@ namespace Ryujinx.Graphics.Gpu.Image } } } - else if (Format == Format.R4G4B4A4Unorm) + else if (formatInfo.Format == Format.R4G4B4A4Unorm) { if (!_context.Capabilities.SupportsR4G4B4A4Format) { @@ -925,9 +1014,9 @@ namespace Ryujinx.Graphics.Gpu.Image } } } - else if (!_context.Capabilities.Supports5BitComponentFormat && Format.Is16BitPacked()) + else if (!_context.Capabilities.Supports5BitComponentFormat && formatInfo.Format.Is16BitPacked()) { - switch (Format) + switch (formatInfo.Format) { case Format.B5G6R5Unorm: case Format.R5G6B5Unorm: @@ -973,8 +1062,8 @@ namespace Ryujinx.Graphics.Gpu.Image int width = Info.Width; int height = Info.Height; - int depth = _depth; - int layers = single ? 1 : _layers; + int depth = Depth; + int layers = single ? 1 : Layers; int levels = single ? 1 : (Info.Levels - level); width = Math.Max(width >> level, 1); @@ -1029,7 +1118,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// True if data was flushed, false otherwise public bool FlushModified(bool tracked = true) { - return TextureCompatibility.CanTextureFlush(Info, _context.Capabilities) && Group.FlushModified(this, tracked); + return TextureCompatibility.CanTextureFlush(this, _context.Capabilities) && Group.FlushModified(this, tracked); } /// @@ -1043,7 +1132,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// Whether or not the flush triggers write tracking. If it doesn't, the texture will not be blacklisted for scaling either. public void Flush(bool tracked) { - if (TextureCompatibility.CanTextureFlush(Info, _context.Capabilities)) + if (TextureCompatibility.CanTextureFlush(this, _context.Capabilities)) { FlushTextureDataToGuest(tracked); } @@ -1299,6 +1388,22 @@ namespace Ryujinx.Graphics.Gpu.Image return result; } + public bool HasImportOverride() + { + if (_importOverride.HasValue) + { + TextureInfoOverride importOverride = _importOverride.Value; + + return importOverride.Width != Info.Width || + importOverride.Height != Info.Height || + importOverride.DepthOrLayers != Info.GetDepthOrLayers() || + importOverride.Levels != Info.Levels || + importOverride.FormatInfo.Format != Info.FormatInfo.Format; + } + + return false; + } + /// /// Gets a texture of the specified target type from this texture. /// This can be used to get an array texture from a non-array texture and vice-versa. @@ -1315,7 +1420,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (_arrayViewTexture == null && IsSameDimensionsTarget(target)) { - FormatInfo formatInfo = TextureCompatibility.ToHostCompatibleFormat(Info, _context.Capabilities); + FormatInfo formatInfo = TextureCompatibility.ToHostCompatibleFormat(Info.FormatInfo, Info.Target, _context.Capabilities); TextureCreateInfo createInfo = new( Info.Width, @@ -1418,7 +1523,7 @@ namespace Ryujinx.Graphics.Gpu.Image foreach (Texture view in viewCopy) { - TextureCreateInfo createInfo = TextureCache.GetCreateInfo(view.Info, _context.Capabilities, ScaleFactor); + TextureCreateInfo createInfo = TextureCache.GetCreateInfo(view.Info, _context.Capabilities, ScaleFactor, null); ITexture newView = parent.HostTexture.CreateView(createInfo, view.FirstLayer + firstLayer, view.FirstLevel + firstLevel); @@ -1453,8 +1558,8 @@ namespace Ryujinx.Graphics.Gpu.Image Height = info.Height; CanForceAnisotropy = CanTextureForceAnisotropy(); - _depth = info.GetDepth(); - _layers = info.GetLayers(); + Depth = info.GetDepth(); + Layers = info.GetLayers(); } /// diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs index b9ff060e2..0a52f934f 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs @@ -772,6 +772,11 @@ namespace Ryujinx.Graphics.Gpu.Image out int firstLevel, flags); + if (overlap.HasImportOverride()) + { + overlapCompatibility = TextureViewCompatibility.Incompatible; + } + if (overlapCompatibility >= TextureViewCompatibility.FormatAlias) { if (overlap.IsView) @@ -831,7 +836,7 @@ namespace Ryujinx.Graphics.Gpu.Image { // Only copy compatible. If there's another choice for a FULLY compatible texture, choose that instead. - texture = new Texture(_context, _physicalMemory, info, sizeInfo, range.Value, scaleMode); + texture = new Texture(_context, _physicalMemory, info, sizeInfo, range.Value, scaleMode, flags.HasFlag(TextureSearchFlags.WithUpscale)); // If the new texture is larger than the existing one, we need to fill the remaining space with CPU data, // otherwise we only need the data that is copied from the existing texture, without loading the CPU data. @@ -880,7 +885,7 @@ namespace Ryujinx.Graphics.Gpu.Image // No match, create a new texture. if (texture == null) { - texture = new Texture(_context, _physicalMemory, info, sizeInfo, range.Value, scaleMode); + texture = new Texture(_context, _physicalMemory, info, sizeInfo, range.Value, scaleMode, flags.HasFlag(TextureSearchFlags.WithUpscale)); // Step 1: Find textures that are view compatible with the new texture. // Any textures that are incompatible will contain garbage data, so they should be removed where possible. @@ -908,6 +913,11 @@ namespace Ryujinx.Graphics.Gpu.Image out int firstLayer, out int firstLevel); + if (overlap.HasImportOverride()) + { + compatibility = TextureViewCompatibility.Incompatible; + } + if (overlap.IsView && compatibility == TextureViewCompatibility.Full) { compatibility = TextureViewCompatibility.CopyOnly; @@ -1023,6 +1033,12 @@ namespace Ryujinx.Graphics.Gpu.Image continue; } + if (texture.HasImportOverride()) + { + // Replaced textures with different parameters are not considered compatible. + continue; + } + // Note: If we allow different sizes for those overlaps, // we need to make sure that the "info" has the correct size for the parent texture here. // Since this is not allowed right now, we don't need to do it. @@ -1046,14 +1062,11 @@ namespace Ryujinx.Graphics.Gpu.Image } else { - TextureCreateInfo createInfo = GetCreateInfo(overlapInfo, _context.Capabilities, overlap.ScaleFactor); - + TextureCreateInfo createInfo = GetCreateInfo(overlapInfo, _context.Capabilities, overlap.ScaleFactor, null); ITexture newView = texture.HostTexture.CreateView(createInfo, oInfo.FirstLayer, oInfo.FirstLevel); overlap.SynchronizeMemory(); - overlap.HostTexture.CopyTo(newView, 0, 0); - overlap.ReplaceView(texture, overlapInfo, newView, oInfo.FirstLayer, oInfo.FirstLevel); } } @@ -1222,10 +1235,30 @@ namespace Ryujinx.Graphics.Gpu.Image /// Texture information /// GPU capabilities /// Texture scale factor, to be applied to the texture size + /// Optional parameters to override the parameter /// The texture creation information - public static TextureCreateInfo GetCreateInfo(TextureInfo info, Capabilities caps, float scale) + public static TextureCreateInfo GetCreateInfo(TextureInfo info, in Capabilities caps, float scale, TextureInfoOverride? infoOverride) { - FormatInfo formatInfo = TextureCompatibility.ToHostCompatibleFormat(info, caps); + int width = info.Width / info.SamplesInX; + int height = info.Height / info.SamplesInY; + + int depth = info.GetDepth() * info.GetLayers(); + int levels = info.Levels; + + FormatInfo formatInfo = info.FormatInfo; + + if (infoOverride.HasValue) + { + TextureInfoOverride overrideValue = infoOverride.Value; + + width = overrideValue.Width; + height = overrideValue.Height; + depth = overrideValue.DepthOrLayers; + levels = overrideValue.Levels; + formatInfo = overrideValue.FormatInfo; + } + + formatInfo = TextureCompatibility.ToHostCompatibleFormat(formatInfo, info.Target, caps); if (info.Target == Target.TextureBuffer && !caps.SupportsSnormBufferTextureFormat) { @@ -1254,11 +1287,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - int width = info.Width / info.SamplesInX; - int height = info.Height / info.SamplesInY; - - int depth = info.GetDepth() * info.GetLayers(); - if (scale != 1f) { width = (int)MathF.Ceiling(width * scale); @@ -1269,7 +1297,7 @@ namespace Ryujinx.Graphics.Gpu.Image width, height, depth, - info.Levels, + levels, info.Samples, formatInfo.BlockWidth, formatInfo.BlockHeight, diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs index 3cdeac9c5..fa54e3eff 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs @@ -50,10 +50,10 @@ namespace Ryujinx.Graphics.Gpu.Image /// Texture information /// Host GPU capabilities /// True if the format is incompatible, false otherwise - public static bool IsFormatHostIncompatible(TextureInfo info, Capabilities caps) + public static bool IsFormatHostIncompatible(TextureInfo info, in Capabilities caps) { Format originalFormat = info.FormatInfo.Format; - return ToHostCompatibleFormat(info, caps).Format != originalFormat; + return ToHostCompatibleFormat(info.FormatInfo, info.Target, caps).Format != originalFormat; } /// @@ -64,10 +64,11 @@ namespace Ryujinx.Graphics.Gpu.Image /// This can be used to convert a incompatible compressed format to the decompressor /// output format. /// - /// Texture information + /// Texture format information + /// Texture dimensions /// Host GPU capabilities /// A host compatible format - public static FormatInfo ToHostCompatibleFormat(TextureInfo info, Capabilities caps) + public static FormatInfo ToHostCompatibleFormat(FormatInfo formatInfo, Target target, in Capabilities caps) { // The host API does not support those compressed formats. // We assume software decompression will be done for those textures, @@ -75,13 +76,13 @@ namespace Ryujinx.Graphics.Gpu.Image if (!caps.SupportsAstcCompression) { - if (info.FormatInfo.Format.IsAstcUnorm()) + if (formatInfo.Format.IsAstcUnorm()) { return GraphicsConfig.EnableTextureRecompression ? new FormatInfo(Format.Bc7Unorm, 4, 4, 16, 4) : new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4); } - else if (info.FormatInfo.Format.IsAstcSrgb()) + else if (formatInfo.Format.IsAstcSrgb()) { return GraphicsConfig.EnableTextureRecompression ? new FormatInfo(Format.Bc7Srgb, 4, 4, 16, 4) @@ -89,9 +90,9 @@ namespace Ryujinx.Graphics.Gpu.Image } } - if (!HostSupportsBcFormat(info.FormatInfo.Format, info.Target, caps)) + if (!HostSupportsBcFormat(formatInfo.Format, target, caps)) { - switch (info.FormatInfo.Format) + switch (formatInfo.Format) { case Format.Bc1RgbaSrgb: case Format.Bc2Srgb: @@ -119,7 +120,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (!caps.SupportsEtc2Compression) { - switch (info.FormatInfo.Format) + switch (formatInfo.Format) { case Format.Etc2RgbaSrgb: case Format.Etc2RgbPtaSrgb: @@ -132,7 +133,7 @@ namespace Ryujinx.Graphics.Gpu.Image } } - if (!caps.SupportsR4G4Format && info.FormatInfo.Format == Format.R4G4Unorm) + if (!caps.SupportsR4G4Format && formatInfo.Format == Format.R4G4Unorm) { if (caps.SupportsR4G4B4A4Format) { @@ -144,19 +145,19 @@ namespace Ryujinx.Graphics.Gpu.Image } } - if (info.FormatInfo.Format == Format.R4G4B4A4Unorm) + if (formatInfo.Format == Format.R4G4B4A4Unorm) { if (!caps.SupportsR4G4B4A4Format) { return new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4); } } - else if (!caps.Supports5BitComponentFormat && info.FormatInfo.Format.Is16BitPacked()) + else if (!caps.Supports5BitComponentFormat && formatInfo.Format.Is16BitPacked()) { - return new FormatInfo(info.FormatInfo.Format.IsBgr() ? Format.B8G8R8A8Unorm : Format.R8G8B8A8Unorm, 1, 1, 4, 4); + return new FormatInfo(formatInfo.Format.IsBgr() ? Format.B8G8R8A8Unorm : Format.R8G8B8A8Unorm, 1, 1, 4, 4); } - return info.FormatInfo; + return formatInfo; } /// @@ -166,7 +167,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// Target usage of the texture /// Host GPU Capabilities /// True if the texture host supports the format with the given target usage, false otherwise - public static bool HostSupportsBcFormat(Format format, Target target, Capabilities caps) + public static bool HostSupportsBcFormat(Format format, Target target, in Capabilities caps) { bool not3DOr3DCompressionSupported = target != Target.Texture3D || caps.Supports3DTextureCompression; @@ -194,15 +195,26 @@ namespace Ryujinx.Graphics.Gpu.Image return true; } + /// + /// Determines whether a texture can flush its data back to guest memory. + /// + /// Texture that will have its data flushed + /// Host GPU Capabilities + /// True if the texture can flush, false otherwise + public static bool CanTextureFlush(Texture texture, in Capabilities caps) + { + return !texture.HasImportOverride() && CanTextureFlush(texture.Info, caps); + } + /// /// Determines whether a texture can flush its data back to guest memory. /// /// Texture information /// Host GPU Capabilities /// True if the texture can flush, false otherwise - public static bool CanTextureFlush(TextureInfo info, Capabilities caps) + private static bool CanTextureFlush(TextureInfo info, in Capabilities caps) { - if (IsFormatHostIncompatible(info, caps)) + if (IsFormatHostIncompatible(info, in caps)) { return false; // Flushing this format is not supported, as it may have been converted to another host format. } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs index 06ca2c599..15b77fc38 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -629,7 +629,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (_flushBuffer == BufferHandle.Null) { - if (!TextureCompatibility.CanTextureFlush(Storage.Info, _context.Capabilities)) + if (!TextureCompatibility.CanTextureFlush(Storage, _context.Capabilities)) { return; } @@ -1661,7 +1661,7 @@ namespace Ryujinx.Graphics.Gpu.Image } } - if (TextureCompatibility.CanTextureFlush(Storage.Info, _context.Capabilities) && !(inBuffer && _flushBufferImported)) + if (TextureCompatibility.CanTextureFlush(Storage, _context.Capabilities) && !(inBuffer && _flushBufferImported)) { FlushSliceRange(false, handle.BaseSlice, handle.BaseSlice + handle.SliceCount, inBuffer, Storage.GetFlushTexture()); } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs index 94d2e0bfc..bfe076526 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs @@ -207,6 +207,16 @@ namespace Ryujinx.Graphics.Gpu.Image return GetLayers(Target, DepthOrLayers); } + /// + /// Gets the number of layers or depth of the texture. + /// Returns 1 for non-array textures, 6 for cubemap textures, and layer faces for cubemap array textures. + /// + /// The number of texture layers or depth + public int GetDepthOrLayers() + { + return GetDepthOrLayers(Target, DepthOrLayers); + } + /// /// Gets the number of layers of the texture. /// Returns 1 for non-array textures, 6 for cubemap textures, and layer faces for cubemap array textures. @@ -234,6 +244,33 @@ namespace Ryujinx.Graphics.Gpu.Image } } + /// + /// Gets the number of layers or depth of the texture. + /// Returns 1 for non-array textures, 6 for cubemap textures, and layer faces for cubemap array textures. + /// + /// Texture target + /// Texture layers if the is a array texture, depth for 3D textures, ignored otherwise + /// The number of texture layers or depth + public static int GetDepthOrLayers(Target target, int depthOrLayers) + { + if (target == Target.Texture2DArray || target == Target.Texture2DMultisampleArray || target == Target.Texture3D) + { + return depthOrLayers; + } + else if (target == Target.CubemapArray) + { + return depthOrLayers * 6; + } + else if (target == Target.Cubemap) + { + return 6; + } + else + { + return 1; + } + } + /// /// Gets the number of 2D slices of the texture. /// Returns 6 for cubemap textures, layer faces for cubemap array textures, and DepthOrLayers for everything else. diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureInfoOverride.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureInfoOverride.cs new file mode 100644 index 000000000..f3d5d89f2 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureInfoOverride.cs @@ -0,0 +1,67 @@ +using System; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// + /// Values that should override parameters. + /// + readonly struct TextureInfoOverride + { + /// + /// Texture width override. + /// + public int Width { get; } + + /// + /// Texture height override. + /// + public int Height { get; } + + /// + /// Texture depth (for 3D textures), or layers count override. + /// + public int DepthOrLayers { get; } + + /// + /// Mipmap levels override. + /// + public int Levels { get; } + + /// + /// Texture format override. + /// + public FormatInfo FormatInfo { get; } + + /// + /// Constructs the texture override structure. + /// + /// Texture width override + /// Texture height override + /// Texture depth (for 3D textures), or layers count override + /// Mipmap levels override + /// Texture format override + public TextureInfoOverride(int width, int height, int depthOrLayers, int levels, FormatInfo formatInfo) + { + Width = width; + Height = height; + DepthOrLayers = depthOrLayers; + Levels = levels; + FormatInfo = formatInfo; + } + + public override bool Equals(object obj) + { + return obj is TextureInfoOverride other && + other.Width == Width && + other.Height == Height && + other.DepthOrLayers == DepthOrLayers && + other.Levels == Levels && + other.FormatInfo.Format == FormatInfo.Format; + } + + public override int GetHashCode() + { + return HashCode.Combine(Width, Height, DepthOrLayers, Levels, FormatInfo.Format); + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureInfoOverrideFlags.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureInfoOverrideFlags.cs new file mode 100644 index 000000000..4f8a20dcb --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureInfoOverrideFlags.cs @@ -0,0 +1,26 @@ +using System; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// + /// Flags controlling which parameters of the texture should be overriden. + /// + [Flags] + enum TextureInfoOverrideFlags + { + /// + /// Nothing should be overriden. + /// + None = 0, + + /// + /// The texture size (width, height, depth and levels) should be overriden. + /// + OverrideSize = 1 << 0, + + /// + /// The texture format should be overriden. + /// + OverrideFormat = 1 << 1 + } +} diff --git a/src/Ryujinx.Graphics.Texture/FileFormats/DdsFileFormat.cs b/src/Ryujinx.Graphics.Texture/FileFormats/DdsFileFormat.cs new file mode 100644 index 000000000..cffa40955 --- /dev/null +++ b/src/Ryujinx.Graphics.Texture/FileFormats/DdsFileFormat.cs @@ -0,0 +1,859 @@ +using Ryujinx.Common.Memory; +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Graphics.Texture.FileFormats +{ + public static class DdsFileFormat + { + private const int StrideAlignment = 4; + + [Flags] + private enum DdsFlags : uint + { + Caps = 1, + Height = 2, + Width = 4, + Pitch = 8, + PixelFormat = 0x1000, + MipMapCount = 0x20000, + LinearSize = 0x80000, + Depth = 0x800000, + } + + [Flags] + private enum DdsCaps : uint + { + Complex = 8, + Texture = 0x1000, + MipMap = 0x400000, + } + + [Flags] + private enum DdsCaps2 : uint + { + None = 0, + CubeMap = 0x200, + CubeMapPositiveX = 0x400, + CubeMapNegativeX = 0x800, + CubeMapPositiveY = 0x1000, + CubeMapNegativeY = 0x2000, + CubeMapPositiveZ = 0x4000, + CubeMapNegativeZ = 0x8000, + Volume = 0x200000, + } + + [Flags] + private enum DdsPfFlags : uint + { + AlphaPixels = 1, + Alpha = 2, + FourCC = 4, + Rgb = 0x40, + Rgba = AlphaPixels | Rgb, + Yuv = 0x200, + Luminance = 0x20000, + BumpDuDv = 0x80000, + } + + private struct DdsPixelFormat + { + public uint Size; + public DdsPfFlags Flags; + public uint FourCC; + public uint RGBBitCount; + public uint RBitMask; + public uint GBitMask; + public uint BBitMask; + public uint ABitMask; + } + + private struct DdsHeader + { + public uint Size; + public DdsFlags Flags; + public uint Height; + public uint Width; + public uint PitchOrLinearSize; + public uint Depth; + public uint MipMapCount; + public Array11 Reserved1; + public DdsPixelFormat DdsPf; + public DdsCaps Caps; + public DdsCaps2 Caps2; + public uint Caps3; + public uint Caps4; + public uint Reserved2; + } + + private enum D3d10ResourceDimension : uint + { + Unknown = 0, + Buffer = 1, + Texture1D = 2, + Texture2D = 3, + Texture3D = 4, + } + + private struct DdsHeaderDxt10 + { + public DxgiFormat DxgiFormat; + public D3d10ResourceDimension ResourceDimension; + public uint MiscFlag; + public uint ArraySize; + public uint MiscFlags2; + } + + private const uint DdsMagic = 0x20534444; + private const uint Dxt1FourCC = 'D' | ('X' << 8) | ('T' << 16) | ('1' << 24); + private const uint Dxt3FourCC = 'D' | ('X' << 8) | ('T' << 16) | ('3' << 24); + private const uint Dxt5FourCC = 'D' | ('X' << 8) | ('T' << 16) | ('5' << 24); + private const uint Dx10FourCC = 'D' | ('X' << 8) | ('1' << 16) | ('0' << 24); + private const uint Bc4UFourCC = 'B' | ('C' << 8) | ('4' << 16) | ('U' << 24); + private const uint Bc4SFourCC = 'B' | ('C' << 8) | ('4' << 16) | ('S' << 24); + private const uint Bc5SFourCC = 'B' | ('C' << 8) | ('5' << 16) | ('S' << 24); + private const uint Ati1FourCC = 'A' | ('T' << 8) | ('I' << 16) | ('1' << 24); + private const uint Ati2FourCC = 'A' | ('T' << 8) | ('I' << 16) | ('2' << 24); + + public static ImageLoadResult TryLoadHeader(ReadOnlySpan ddsData, out ImageParameters parameters) + { + return TryLoadHeaderImpl(ddsData, out parameters, out _); + } + + private static ImageLoadResult TryLoadHeaderImpl(ReadOnlySpan ddsData, out ImageParameters parameters, out int dataOffset) + { + parameters = default; + dataOffset = 0; + + if (ddsData.Length < 4 + Unsafe.SizeOf()) + { + return ImageLoadResult.DataTooShort; + } + + uint magic = ddsData.Read(); + DdsHeader header = ddsData[4..].Read(); + + if (magic != DdsMagic || + header.Size != Unsafe.SizeOf() || + header.DdsPf.Size != Unsafe.SizeOf()) + { + return ImageLoadResult.CorruptedHeader; + } + + int depth = header.Flags.HasFlag(DdsFlags.Depth) ? (int)header.Depth : 1; + int levels = header.Flags.HasFlag(DdsFlags.MipMapCount) ? (int)header.MipMapCount : 1; + int layers = 1; + ImageDimensions dimensions = header.Flags.HasFlag(DdsFlags.Depth) ? ImageDimensions.Dim3D : ImageDimensions.Dim2D; + ImageFormat format = GetFormat(header.DdsPf); + + if (header.Caps2.HasFlag(DdsCaps2.CubeMap)) + { + layers = 6; + dimensions = ImageDimensions.DimCube; + } + + dataOffset = 4 + Unsafe.SizeOf(); + + if (header.DdsPf.Flags.HasFlag(DdsPfFlags.FourCC) && header.DdsPf.FourCC == Dx10FourCC) + { + if (ddsData.Length < 4 + Unsafe.SizeOf() + Unsafe.SizeOf()) + { + return ImageLoadResult.DataTooShort; + } + + DdsHeaderDxt10 headerDxt10 = ddsData[dataOffset..].Read(); + + if (dimensions != ImageDimensions.Dim3D) + { + if (headerDxt10.MiscFlag == 4u) + { + // Cube array. + layers = (int)Math.Max(1, headerDxt10.ArraySize) * 6; + dimensions = headerDxt10.ArraySize > 1 ? ImageDimensions.DimCubeArray : ImageDimensions.DimCube; + } + else + { + // 2D array. + layers = (int)Math.Max(1, headerDxt10.ArraySize); + dimensions = headerDxt10.ArraySize > 1 ? ImageDimensions.Dim2DArray : ImageDimensions.Dim2D; + } + } + + format = ConvertToImageFormat(headerDxt10.DxgiFormat); + + dataOffset += Unsafe.SizeOf(); + } + + parameters = new((int)header.Width, (int)header.Height, depth * layers, levels, format, dimensions); + + return ImageLoadResult.Success; + } + + public static int CalculateSize(in ImageParameters parameters) + { + return CalculateSizeInternal(parameters, StrideAlignment); + } + + private static int CalculateSizeInternal(in ImageParameters parameters, int strideAlignment) + { + if (parameters.Format == ImageFormat.Unknown) + { + return 0; + } + + int size = 0; + (int bw, int bh, int bpp) = GetBlockSizeAndBpp(parameters.Format); + + for (int l = 0; l < parameters.Levels; l++) + { + int w = Math.Max(1, parameters.Width >> l); + int h = Math.Max(1, parameters.Height >> l); + int d = parameters.Dimensions == ImageDimensions.Dim3D ? Math.Max(1, parameters.DepthOrLayers >> l) : parameters.DepthOrLayers; + + w = (w + bw - 1) / bw; + h = (h + bh - 1) / bh; + + int stride = (w * bpp + strideAlignment - 1) & -strideAlignment; + size += stride * h * d; + } + + return size; + } + + private static int CalculateStride(in ImageParameters parameters, int level) + { + if (parameters.Format == ImageFormat.Unknown) + { + return 0; + } + + (int bw, _, int bpp) = GetBlockSizeAndBpp(parameters.Format); + + int w = Math.Max(1, parameters.Width >> level); + w = (w + bw - 1) / bw; + + return w * bpp; + } + + public static ImageLoadResult TryLoadData(ReadOnlySpan ddsData, Span output) + { + ImageLoadResult result = TryLoadHeaderImpl(ddsData, out ImageParameters parameters, out int dataOffset); + + if (result != ImageLoadResult.Success) + { + return result; + } + + if (parameters.Format == ImageFormat.Unknown) + { + return ImageLoadResult.UnsupportedFormat; + } + + int size = CalculateSize(parameters); + int inSize = CalculateSizeInternal(parameters, 1); + + // Some basic validation for completely bogus sizes. + if (inSize <= 0 || dataOffset + inSize <= 0) + { + return ImageLoadResult.CorruptedHeader; + } + + if (dataOffset + inSize > ddsData.Length) + { + return ImageLoadResult.DataTooShort; + } + + if (output.Length < size) + { + return ImageLoadResult.OutputTooShort; + } + + if ((parameters.DepthOrLayers > 1 && parameters.Dimensions != ImageDimensions.Dim3D) || size != inSize) + { + int inOffset = dataOffset; + + bool isArray = IsArray(parameters.Dimensions) || parameters.Dimensions == ImageDimensions.DimCube; + int layers = isArray ? parameters.DepthOrLayers : 1; + + for (int z = 0; z < layers; z++) + { + for (int l = 0; l < parameters.Levels; l++) + { + (int sliceOffset, int sliceSize) = GetSlice(parameters, z, l); + inOffset += CopyData(output, ddsData, sliceOffset, inOffset, sliceSize, CalculateStride(parameters, l)); + } + } + } + else + { + CopyData(output, ddsData, 0, dataOffset, size, CalculateStride(parameters, 0)); + } + + return ImageLoadResult.Success; + } + + private static int CopyData(Span destination, ReadOnlySpan source, int dstOffset, int srcOffset, int size, int stride) + { + int strideAligned = (stride + StrideAlignment - 1) & -StrideAlignment; + + if (stride != strideAligned) + { + int rows = size / strideAligned; + + for (int y = 0; y < rows; y++) + { + source.Slice(srcOffset + y * stride, stride).CopyTo(destination.Slice(dstOffset + y * strideAligned, stride)); + } + + return rows * stride; + } + else + { + source.Slice(srcOffset, size).CopyTo(destination.Slice(dstOffset, size)); + + return size; + } + } + + public static void Save(Stream output, ImageParameters parameters, ReadOnlySpan data) + { + DdsFlags flags = DdsFlags.Caps | DdsFlags.Height | DdsFlags.Width | DdsFlags.PixelFormat; + DdsCaps caps = DdsCaps.Texture; + DdsCaps2 caps2 = DdsCaps2.None; + + if (parameters.Levels > 1) + { + flags |= DdsFlags.MipMapCount; + caps |= DdsCaps.MipMap | DdsCaps.Complex; + } + + if (parameters.Dimensions == ImageDimensions.DimCube) + { + caps2 |= DdsCaps2.CubeMap | DdsCaps2.CubeMapPositiveX; + + if (parameters.DepthOrLayers > 1) + { + caps2 |= DdsCaps2.CubeMapNegativeX; + } + + if (parameters.DepthOrLayers > 2) + { + caps2 |= DdsCaps2.CubeMapPositiveY; + } + + if (parameters.DepthOrLayers > 3) + { + caps2 |= DdsCaps2.CubeMapNegativeY; + } + + if (parameters.DepthOrLayers > 4) + { + caps2 |= DdsCaps2.CubeMapPositiveZ; + } + + if (parameters.DepthOrLayers > 5) + { + caps2 |= DdsCaps2.CubeMapNegativeZ; + } + } + else if (parameters.Dimensions == ImageDimensions.Dim3D) + { + flags |= DdsFlags.Depth; + caps2 |= DdsCaps2.Volume; + } + + bool isArray = IsArray(parameters.Dimensions); + bool needsDxt10Header = isArray || !IsLegacyCompatibleFormat(parameters.Format); + + DdsPixelFormat pixelFormat = needsDxt10Header ? CreateDx10PixelFormat() : CreatePixelFormat(parameters.Format); + + (int bw, int bh, int bpp) = GetBlockSizeAndBpp(parameters.Format); + + int pitch = (parameters.Width + bw - 1) / bw * bpp; + int pitchOrLinearSize = pitch; + + if (bw > 1 || bh > 1) + { + flags |= DdsFlags.LinearSize; + pitchOrLinearSize *= (parameters.Height + bh - 1) / bh * parameters.DepthOrLayers; + } + else + { + flags |= DdsFlags.Pitch; + } + + DdsHeader header = new() + { + Size = (uint)Unsafe.SizeOf(), + Flags = flags, + Height = (uint)parameters.Height, + Width = (uint)parameters.Width, + PitchOrLinearSize = (uint)pitchOrLinearSize, + Depth = (uint)parameters.DepthOrLayers, + MipMapCount = (uint)parameters.Levels, + Reserved1 = default, + DdsPf = pixelFormat, + Caps = caps, + Caps2 = caps2, + Caps3 = 0, + Caps4 = 0, + Reserved2 = 0, + }; + + output.Write(DdsMagic); + output.Write(header); + + if (needsDxt10Header) + { + output.Write(CreateDxt10Header(parameters.Format, parameters.Dimensions, parameters.DepthOrLayers)); + } + + if ((parameters.DepthOrLayers > 1 && parameters.Dimensions != ImageDimensions.Dim3D) || bpp < StrideAlignment) + { + // On DDS, the order is: + // [Layer 0 Level 0] [Layer 0 Level 1] [Layer 1 Level 0] [Layer 1 Level 1] + // While on the input data, the order is: + // [Layer 0 Level 0] [Layer 1 Level 0] [Layer 0 Level 1] [Layer 1 Level 1] + + int layers = isArray || parameters.Dimensions == ImageDimensions.DimCube ? parameters.DepthOrLayers : 1; + + for (int z = 0; z < layers; z++) + { + for (int l = 0; l < parameters.Levels; l++) + { + (int sliceOffset, int sliceSize) = GetSlice(parameters, z, l); + pitch = (Math.Max(1, parameters.Width >> l) + bw - 1) / bw * bpp; + WriteData(output, data.Slice(sliceOffset, sliceSize), pitch); + } + } + } + else + { + WriteData(output, data, pitch); + } + } + + private static void WriteData(Stream output, ReadOnlySpan data, int stride) + { + int strideAligned = (stride + StrideAlignment - 1) & -StrideAlignment; + + if (stride != strideAligned) + { + for (int i = 0; i < data.Length; i += strideAligned) + { + output.Write(data.Slice(i, stride)); + } + } + else + { + output.Write(data); + } + } + + private static (int, int) GetSlice(ImageParameters parameters, int layer, int level) + { + int size = 0; + int sliceSize = 0; + int depth, layers; + + if (parameters.Dimensions == ImageDimensions.Dim3D) + { + depth = parameters.DepthOrLayers; + layers = 1; + } + else + { + depth = 1; + layers = parameters.DepthOrLayers; + } + + (int bw, int bh, int bpp) = GetBlockSizeAndBpp(parameters.Format); + + for (int l = 0; l <= level; l++) + { + int w = Math.Max(1, parameters.Width >> l); + int h = Math.Max(1, parameters.Height >> l); + int d = Math.Max(1, depth >> l); + + w = (w + bw - 1) / bw; + h = (h + bh - 1) / bh; + + for (int z = 0; z < (l < level ? layers : layer + 1); z++) + { + int stride = (w * bpp + StrideAlignment - 1) & -StrideAlignment; + sliceSize = stride * h * d; + size += sliceSize; + } + } + + return (size - sliceSize, sliceSize); + } + + private static void Write(this Stream stream, T value) where T : unmanaged + { + stream.Write(MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref value, 1))); + } + + private static T Read(this ReadOnlySpan span) where T : unmanaged + { + return MemoryMarshal.Cast(span)[0]; + } + + private static bool IsArray(ImageDimensions dimensions) + { + return dimensions == ImageDimensions.Dim2DArray || dimensions == ImageDimensions.DimCubeArray; + } + + private static DdsHeaderDxt10 CreateDxt10Header(ImageFormat format, ImageDimensions dimensions, int depthOrLayers) + { + D3d10ResourceDimension resourceDimension = dimensions == ImageDimensions.Dim3D + ? D3d10ResourceDimension.Texture3D + : D3d10ResourceDimension.Texture2D; + + uint arraySize = 1; + + if (dimensions == ImageDimensions.Dim2DArray) + { + arraySize = (uint)depthOrLayers; + } + else if (dimensions == ImageDimensions.DimCubeArray) + { + arraySize = (uint)depthOrLayers / 6; + } + + return new DdsHeaderDxt10() + { + DxgiFormat = ConvertToDxgiFormat(format), + ResourceDimension = resourceDimension, + MiscFlag = dimensions == ImageDimensions.DimCube || dimensions == ImageDimensions.DimCubeArray ? 4u : 0u, + ArraySize = arraySize, + MiscFlags2 = 1, // Straight alpha. + }; + } + + private static DdsPixelFormat CreateDx10PixelFormat() + { + return new DdsPixelFormat() + { + Size = (uint)Unsafe.SizeOf(), + Flags = DdsPfFlags.FourCC, + FourCC = Dx10FourCC, + }; + } + + private static DdsPixelFormat CreatePixelFormat(ImageFormat format) + { + DdsPixelFormat pf = new() + { + Size = (uint)Unsafe.SizeOf(), + }; + + switch (format) + { + case ImageFormat.Bc1RgbaUnorm: + pf.Flags = DdsPfFlags.FourCC; + pf.FourCC = Dxt1FourCC; + break; + case ImageFormat.Bc2Unorm: + pf.Flags = DdsPfFlags.FourCC; + pf.FourCC = Dxt3FourCC; + break; + case ImageFormat.Bc3Unorm: + pf.Flags = DdsPfFlags.FourCC; + pf.FourCC = Dxt5FourCC; + break; + case ImageFormat.Bc4Snorm: + pf.Flags = DdsPfFlags.FourCC; + pf.FourCC = Bc4SFourCC; + break; + case ImageFormat.Bc4Unorm: + pf.Flags = DdsPfFlags.FourCC; + pf.FourCC = Bc4UFourCC; + break; + case ImageFormat.Bc5Snorm: + pf.Flags = DdsPfFlags.FourCC; + pf.FourCC = Bc5SFourCC; + break; + case ImageFormat.Bc5Unorm: + pf.Flags = DdsPfFlags.FourCC; + pf.FourCC = Ati2FourCC; + break; + case ImageFormat.R8Unorm: + pf.Flags = DdsPfFlags.Luminance; + pf.RGBBitCount = 8; + pf.RBitMask = 0xffu; + pf.GBitMask = 0; + pf.BBitMask = 0; + pf.ABitMask = 0; + break; + case ImageFormat.R8G8Unorm: + pf.Flags = DdsPfFlags.BumpDuDv; + pf.RGBBitCount = 16; + pf.RBitMask = 0xffu; + pf.GBitMask = 0xffu << 8; + pf.BBitMask = 0; + pf.ABitMask = 0; + break; + case ImageFormat.R8G8B8A8Unorm: + pf.Flags = DdsPfFlags.Rgba; + pf.RGBBitCount = 32; + pf.RBitMask = 0xffu; + pf.GBitMask = 0xffu << 8; + pf.BBitMask = 0xffu << 16; + pf.ABitMask = 0xffu << 24; + break; + case ImageFormat.B8G8R8A8Unorm: + pf.Flags = DdsPfFlags.Rgba; + pf.RGBBitCount = 32; + pf.RBitMask = 0xffu << 16; + pf.GBitMask = 0xffu << 8; + pf.BBitMask = 0xffu; + pf.ABitMask = 0xffu << 24; + break; + case ImageFormat.R5G6B5Unorm: + pf.Flags = DdsPfFlags.Rgb; + pf.RGBBitCount = 16; + pf.RBitMask = 0x1fu << 11; + pf.GBitMask = 0x3fu << 5; + pf.BBitMask = 0x1fu; + break; + case ImageFormat.R5G5B5A1Unorm: + pf.Flags = DdsPfFlags.Rgba; + pf.RGBBitCount = 16; + pf.RBitMask = 0x1fu << 10; + pf.GBitMask = 0x1fu << 5; + pf.BBitMask = 0x1fu; + pf.ABitMask = 1u << 15; + break; + case ImageFormat.R4G4B4A4Unorm: + pf.Flags = DdsPfFlags.Rgba; + pf.RGBBitCount = 16; + pf.RBitMask = 0xfu << 8; + pf.GBitMask = 0xfu << 4; + pf.BBitMask = 0xfu; + pf.ABitMask = 0xfu << 12; + break; + default: + throw new ArgumentException($"Can't encode format \"{format}\" on legacy pixel format structure."); + } + + return pf; + } + + private static (int, int, int) GetBlockSizeAndBpp(ImageFormat format) + { + int bw = 1; + int bh = 1; + int bpp = 0; + + switch (format) + { + case ImageFormat.Bc1RgbaSrgb: + case ImageFormat.Bc1RgbaUnorm: + case ImageFormat.Bc4Snorm: + case ImageFormat.Bc4Unorm: + bw = bh = 4; + bpp = 8; + break; + case ImageFormat.Bc2Srgb: + case ImageFormat.Bc2Unorm: + case ImageFormat.Bc3Srgb: + case ImageFormat.Bc3Unorm: + case ImageFormat.Bc5Snorm: + case ImageFormat.Bc5Unorm: + case ImageFormat.Bc7Srgb: + case ImageFormat.Bc7Unorm: + bw = bh = 4; + bpp = 16; + break; + case ImageFormat.R8Unorm: + bpp = 1; + break; + case ImageFormat.R8G8Unorm: + case ImageFormat.R5G6B5Unorm: + case ImageFormat.R5G5B5A1Unorm: + case ImageFormat.R4G4B4A4Unorm: + bpp = 2; + break; + case ImageFormat.R8G8B8A8Srgb: + case ImageFormat.R8G8B8A8Unorm: + case ImageFormat.B8G8R8A8Srgb: + case ImageFormat.B8G8R8A8Unorm: + bpp = 4; + break; + } + + if (bpp == 0) + { + throw new ArgumentException($"Invalid format {format}."); + } + + return (bw, bh, bpp); + } + + private static bool IsLegacyCompatibleFormat(ImageFormat format) + { + switch (format) + { + case ImageFormat.Bc1RgbaUnorm: + case ImageFormat.Bc2Unorm: + case ImageFormat.Bc3Unorm: + case ImageFormat.Bc4Snorm: + case ImageFormat.Bc4Unorm: + case ImageFormat.Bc5Snorm: + case ImageFormat.Bc5Unorm: + case ImageFormat.R8G8B8A8Unorm: + case ImageFormat.B8G8R8A8Unorm: + case ImageFormat.R5G6B5Unorm: + case ImageFormat.R5G5B5A1Unorm: + case ImageFormat.R4G4B4A4Unorm: + return true; + } + + return false; + } + + private static DxgiFormat ConvertToDxgiFormat(ImageFormat format) + { + return format switch + { + ImageFormat.Bc1RgbaSrgb => DxgiFormat.FormatBC1UnormSrgb, + ImageFormat.Bc1RgbaUnorm => DxgiFormat.FormatBC1Unorm, + ImageFormat.Bc2Srgb => DxgiFormat.FormatBC2UnormSrgb, + ImageFormat.Bc2Unorm => DxgiFormat.FormatBC2Unorm, + ImageFormat.Bc3Srgb => DxgiFormat.FormatBC3UnormSrgb, + ImageFormat.Bc3Unorm => DxgiFormat.FormatBC3Unorm, + ImageFormat.Bc4Snorm => DxgiFormat.FormatBC4Snorm, + ImageFormat.Bc4Unorm => DxgiFormat.FormatBC4Unorm, + ImageFormat.Bc5Snorm => DxgiFormat.FormatBC5Snorm, + ImageFormat.Bc5Unorm => DxgiFormat.FormatBC5Unorm, + ImageFormat.Bc7Srgb => DxgiFormat.FormatBC7UnormSrgb, + ImageFormat.Bc7Unorm => DxgiFormat.FormatBC7Unorm, + ImageFormat.R8Unorm => DxgiFormat.FormatR8Unorm, + ImageFormat.R8G8Unorm => DxgiFormat.FormatR8G8Unorm, + ImageFormat.R8G8B8A8Srgb => DxgiFormat.FormatR8G8B8A8UnormSrgb, + ImageFormat.R8G8B8A8Unorm => DxgiFormat.FormatR8G8B8A8Unorm, + ImageFormat.B8G8R8A8Srgb => DxgiFormat.FormatB8G8R8A8UnormSrgb, + ImageFormat.B8G8R8A8Unorm => DxgiFormat.FormatB8G8R8A8Unorm, + ImageFormat.R5G6B5Unorm => DxgiFormat.FormatB5G6R5Unorm, + ImageFormat.R5G5B5A1Unorm => DxgiFormat.FormatB5G5R5A1Unorm, + ImageFormat.R4G4B4A4Unorm => DxgiFormat.FormatB4G4R4A4Unorm, + _ => DxgiFormat.FormatUnknown, + }; + } + + private static ImageFormat GetFormat(DdsPixelFormat pixelFormat) + { + if (pixelFormat.Flags.HasFlag(DdsPfFlags.FourCC)) + { + return pixelFormat.FourCC switch + { + Dxt1FourCC => ImageFormat.Bc1RgbaUnorm, + Dxt3FourCC => ImageFormat.Bc2Unorm, + Dxt5FourCC => ImageFormat.Bc3Unorm, + Bc4SFourCC => ImageFormat.Bc4Snorm, + Bc4UFourCC or Ati1FourCC => ImageFormat.Bc4Unorm, + Bc5SFourCC => ImageFormat.Bc5Snorm, + Ati2FourCC => ImageFormat.Bc5Unorm, + _ => ImageFormat.Unknown, + }; + } + else + { + if (pixelFormat.Flags == DdsPfFlags.Luminance && + pixelFormat.RGBBitCount == 8 && + pixelFormat.RBitMask == 0xffu && + pixelFormat.GBitMask == 0 && + pixelFormat.BBitMask == 0) + { + return ImageFormat.R8Unorm; + } + else if (pixelFormat.Flags == DdsPfFlags.BumpDuDv && + pixelFormat.RGBBitCount == 16 && + pixelFormat.RBitMask == 0xffu && + pixelFormat.GBitMask == 0xffu << 8 && + pixelFormat.BBitMask == 0) + { + return ImageFormat.R8G8Unorm; + } + else if ((pixelFormat.Flags & DdsPfFlags.Rgba) == DdsPfFlags.Rgba && + pixelFormat.RGBBitCount == 32 && + pixelFormat.RBitMask == 0xffu && + pixelFormat.GBitMask == 0xffu << 8 && + pixelFormat.BBitMask == 0xffu << 16 && + pixelFormat.ABitMask == 0xffu << 24) + { + return ImageFormat.R8G8B8A8Unorm; + } + else if ((pixelFormat.Flags & DdsPfFlags.Rgba) == DdsPfFlags.Rgba && + pixelFormat.RGBBitCount == 32 && + pixelFormat.RBitMask == 0xffu << 16 && + pixelFormat.GBitMask == 0xffu << 8 && + pixelFormat.BBitMask == 0xffu && + pixelFormat.ABitMask == 0xffu << 24) + { + return ImageFormat.B8G8R8A8Unorm; + } + else if ((pixelFormat.Flags & DdsPfFlags.Rgba) == DdsPfFlags.Rgb && + pixelFormat.RGBBitCount == 16 && + pixelFormat.RBitMask == 0x1fu << 11 && + pixelFormat.GBitMask == 0x3fu << 5 && + pixelFormat.BBitMask == 0x1fu) + { + return ImageFormat.R5G6B5Unorm; + } + else if ((pixelFormat.Flags & DdsPfFlags.Rgba) == DdsPfFlags.Rgba && + pixelFormat.RGBBitCount == 16 && + pixelFormat.RBitMask == 0x1fu << 10 && + pixelFormat.GBitMask == 0x1fu << 5 && + pixelFormat.BBitMask == 0x1fu && + pixelFormat.ABitMask == 1u << 15) + { + return ImageFormat.R5G5B5A1Unorm; + } + else if ((pixelFormat.Flags & DdsPfFlags.Rgba) == DdsPfFlags.Rgba && + pixelFormat.RGBBitCount == 16 && + pixelFormat.RBitMask == 0xfu << 8 && + pixelFormat.GBitMask == 0xfu << 4 && + pixelFormat.BBitMask == 0xfu && + pixelFormat.ABitMask == 0xfu << 12) + { + return ImageFormat.R4G4B4A4Unorm; + } + } + + return ImageFormat.Unknown; + } + + private static ImageFormat ConvertToImageFormat(DxgiFormat format) + { + return format switch + { + DxgiFormat.FormatBC1UnormSrgb => ImageFormat.Bc1RgbaSrgb, + DxgiFormat.FormatBC1Unorm => ImageFormat.Bc1RgbaUnorm, + DxgiFormat.FormatBC2UnormSrgb => ImageFormat.Bc2Srgb, + DxgiFormat.FormatBC2Unorm => ImageFormat.Bc2Unorm, + DxgiFormat.FormatBC3UnormSrgb => ImageFormat.Bc3Srgb, + DxgiFormat.FormatBC3Unorm => ImageFormat.Bc3Unorm, + DxgiFormat.FormatBC4Snorm => ImageFormat.Bc4Snorm, + DxgiFormat.FormatBC4Unorm => ImageFormat.Bc4Unorm, + DxgiFormat.FormatBC5Snorm => ImageFormat.Bc5Snorm, + DxgiFormat.FormatBC5Unorm => ImageFormat.Bc5Unorm, + DxgiFormat.FormatBC7UnormSrgb => ImageFormat.Bc7Srgb, + DxgiFormat.FormatBC7Unorm => ImageFormat.Bc7Unorm, + DxgiFormat.FormatR8Unorm => ImageFormat.R8Unorm, + DxgiFormat.FormatR8G8Unorm => ImageFormat.R8G8Unorm, + DxgiFormat.FormatR8G8B8A8UnormSrgb => ImageFormat.R8G8B8A8Srgb, + DxgiFormat.FormatR8G8B8A8Unorm => ImageFormat.R8G8B8A8Unorm, + DxgiFormat.FormatB8G8R8A8UnormSrgb => ImageFormat.B8G8R8A8Srgb, + DxgiFormat.FormatB8G8R8A8Unorm => ImageFormat.B8G8R8A8Unorm, + DxgiFormat.FormatB5G6R5Unorm => ImageFormat.R5G6B5Unorm, + DxgiFormat.FormatB5G5R5A1Unorm => ImageFormat.R5G5B5A1Unorm, + DxgiFormat.FormatB4G4R4A4Unorm => ImageFormat.R4G4B4A4Unorm, + _ => ImageFormat.Unknown, + }; + } + } +} diff --git a/src/Ryujinx.Graphics.Texture/FileFormats/DxgiFormat.cs b/src/Ryujinx.Graphics.Texture/FileFormats/DxgiFormat.cs new file mode 100644 index 000000000..964a531ea --- /dev/null +++ b/src/Ryujinx.Graphics.Texture/FileFormats/DxgiFormat.cs @@ -0,0 +1,125 @@ +namespace Ryujinx.Graphics.Texture.FileFormats +{ + enum DxgiFormat + { + FormatUnknown = 0x0, + FormatR32G32B32A32Typeless = 0x1, + FormatR32G32B32A32Float = 0x2, + FormatR32G32B32A32Uint = 0x3, + FormatR32G32B32A32Sint = 0x4, + FormatR32G32B32Typeless = 0x5, + FormatR32G32B32Float = 0x6, + FormatR32G32B32Uint = 0x7, + FormatR32G32B32Sint = 0x8, + FormatR16G16B16A16Typeless = 0x9, + FormatR16G16B16A16Float = 0xA, + FormatR16G16B16A16Unorm = 0xB, + FormatR16G16B16A16Uint = 0xC, + FormatR16G16B16A16Snorm = 0xD, + FormatR16G16B16A16Sint = 0xE, + FormatR32G32Typeless = 0xF, + FormatR32G32Float = 0x10, + FormatR32G32Uint = 0x11, + FormatR32G32Sint = 0x12, + FormatR32G8X24Typeless = 0x13, + FormatD32FloatS8X24Uint = 0x14, + FormatR32FloatX8X24Typeless = 0x15, + FormatX32TypelessG8X24Uint = 0x16, + FormatR10G10B10A2Typeless = 0x17, + FormatR10G10B10A2Unorm = 0x18, + FormatR10G10B10A2Uint = 0x19, + FormatR11G11B10Float = 0x1A, + FormatR8G8B8A8Typeless = 0x1B, + FormatR8G8B8A8Unorm = 0x1C, + FormatR8G8B8A8UnormSrgb = 0x1D, + FormatR8G8B8A8Uint = 0x1E, + FormatR8G8B8A8Snorm = 0x1F, + FormatR8G8B8A8Sint = 0x20, + FormatR16G16Typeless = 0x21, + FormatR16G16Float = 0x22, + FormatR16G16Unorm = 0x23, + FormatR16G16Uint = 0x24, + FormatR16G16Snorm = 0x25, + FormatR16G16Sint = 0x26, + FormatR32Typeless = 0x27, + FormatD32Float = 0x28, + FormatR32Float = 0x29, + FormatR32Uint = 0x2A, + FormatR32Sint = 0x2B, + FormatR24G8Typeless = 0x2C, + FormatD24UnormS8Uint = 0x2D, + FormatR24UnormX8Typeless = 0x2E, + FormatX24TypelessG8Uint = 0x2F, + FormatR8G8Typeless = 0x30, + FormatR8G8Unorm = 0x31, + FormatR8G8Uint = 0x32, + FormatR8G8Snorm = 0x33, + FormatR8G8Sint = 0x34, + FormatR16Typeless = 0x35, + FormatR16Float = 0x36, + FormatD16Unorm = 0x37, + FormatR16Unorm = 0x38, + FormatR16Uint = 0x39, + FormatR16Snorm = 0x3A, + FormatR16Sint = 0x3B, + FormatR8Typeless = 0x3C, + FormatR8Unorm = 0x3D, + FormatR8Uint = 0x3E, + FormatR8Snorm = 0x3F, + FormatR8Sint = 0x40, + FormatA8Unorm = 0x41, + FormatR1Unorm = 0x42, + FormatR9G9B9E5Sharedexp = 0x43, + FormatR8G8B8G8Unorm = 0x44, + FormatG8R8G8B8Unorm = 0x45, + FormatBC1Typeless = 0x46, + FormatBC1Unorm = 0x47, + FormatBC1UnormSrgb = 0x48, + FormatBC2Typeless = 0x49, + FormatBC2Unorm = 0x4A, + FormatBC2UnormSrgb = 0x4B, + FormatBC3Typeless = 0x4C, + FormatBC3Unorm = 0x4D, + FormatBC3UnormSrgb = 0x4E, + FormatBC4Typeless = 0x4F, + FormatBC4Unorm = 0x50, + FormatBC4Snorm = 0x51, + FormatBC5Typeless = 0x52, + FormatBC5Unorm = 0x53, + FormatBC5Snorm = 0x54, + FormatB5G6R5Unorm = 0x55, + FormatB5G5R5A1Unorm = 0x56, + FormatB8G8R8A8Unorm = 0x57, + FormatB8G8R8X8Unorm = 0x58, + FormatR10G10B10XRBiasA2Unorm = 0x59, + FormatB8G8R8A8Typeless = 0x5A, + FormatB8G8R8A8UnormSrgb = 0x5B, + FormatB8G8R8X8Typeless = 0x5C, + FormatB8G8R8X8UnormSrgb = 0x5D, + FormatBC6HTypeless = 0x5E, + FormatBC6HUF16 = 0x5F, + FormatBC6HSF16 = 0x60, + FormatBC7Typeless = 0x61, + FormatBC7Unorm = 0x62, + FormatBC7UnormSrgb = 0x63, + FormatAyuv = 0x64, + FormatY410 = 0x65, + FormatY416 = 0x66, + FormatNV12 = 0x67, + FormatP010 = 0x68, + FormatP016 = 0x69, + Format420Opaque = 0x6A, + FormatYuy2 = 0x6B, + FormatY210 = 0x6C, + FormatY216 = 0x6D, + FormatNV11 = 0x6E, + FormatAI44 = 0x6F, + FormatIA44 = 0x70, + FormatP8 = 0x71, + FormatA8P8 = 0x72, + FormatB4G4R4A4Unorm = 0x73, + FormatP208 = 0x82, + FormatV208 = 0x83, + FormatV408 = 0x84, + } +} diff --git a/src/Ryujinx.Graphics.Texture/FileFormats/ImageDimensions.cs b/src/Ryujinx.Graphics.Texture/FileFormats/ImageDimensions.cs new file mode 100644 index 000000000..33880e03e --- /dev/null +++ b/src/Ryujinx.Graphics.Texture/FileFormats/ImageDimensions.cs @@ -0,0 +1,11 @@ +namespace Ryujinx.Graphics.Texture.FileFormats +{ + public enum ImageDimensions + { + Dim2D, + Dim2DArray, + Dim3D, + DimCube, + DimCubeArray, + } +} diff --git a/src/Ryujinx.Graphics.Texture/FileFormats/ImageFormat.cs b/src/Ryujinx.Graphics.Texture/FileFormats/ImageFormat.cs new file mode 100644 index 000000000..ef853de12 --- /dev/null +++ b/src/Ryujinx.Graphics.Texture/FileFormats/ImageFormat.cs @@ -0,0 +1,28 @@ +namespace Ryujinx.Graphics.Texture.FileFormats +{ + public enum ImageFormat + { + Unknown, + Bc1RgbaSrgb, + Bc1RgbaUnorm, + Bc2Srgb, + Bc2Unorm, + Bc3Srgb, + Bc3Unorm, + Bc4Unorm, + Bc4Snorm, + Bc5Unorm, + Bc5Snorm, + Bc7Srgb, + Bc7Unorm, + R8Unorm, + R8G8Unorm, + R8G8B8A8Srgb, + R8G8B8A8Unorm, + B8G8R8A8Srgb, + B8G8R8A8Unorm, + R5G6B5Unorm, + R5G5B5A1Unorm, + R4G4B4A4Unorm, + } +} diff --git a/src/Ryujinx.Graphics.Texture/FileFormats/ImageLoadResult.cs b/src/Ryujinx.Graphics.Texture/FileFormats/ImageLoadResult.cs new file mode 100644 index 000000000..724693a1c --- /dev/null +++ b/src/Ryujinx.Graphics.Texture/FileFormats/ImageLoadResult.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.Graphics.Texture.FileFormats +{ + public enum ImageLoadResult + { + Success, + CorruptedHeader, + CorruptedData, + DataTooShort, + OutputTooShort, + UnsupportedFormat, + } +} diff --git a/src/Ryujinx.Graphics.Texture/FileFormats/ImageParameters.cs b/src/Ryujinx.Graphics.Texture/FileFormats/ImageParameters.cs new file mode 100644 index 000000000..5c350f966 --- /dev/null +++ b/src/Ryujinx.Graphics.Texture/FileFormats/ImageParameters.cs @@ -0,0 +1,22 @@ +namespace Ryujinx.Graphics.Texture.FileFormats +{ + public readonly struct ImageParameters + { + public int Width { get; } + public int Height { get; } + public int DepthOrLayers { get; } + public int Levels { get; } + public ImageFormat Format { get; } + public ImageDimensions Dimensions { get; } + + public ImageParameters(int width, int height, int depthOrLayers, int levels, ImageFormat format, ImageDimensions dimensions) + { + Width = width; + Height = height; + DepthOrLayers = depthOrLayers; + Levels = levels; + Format = format; + Dimensions = dimensions; + } + } +} diff --git a/src/Ryujinx.Graphics.Texture/FileFormats/PngFileFormat.cs b/src/Ryujinx.Graphics.Texture/FileFormats/PngFileFormat.cs new file mode 100644 index 000000000..c6ab6d9dd --- /dev/null +++ b/src/Ryujinx.Graphics.Texture/FileFormats/PngFileFormat.cs @@ -0,0 +1,757 @@ +using System; +using System.Buffers.Binary; +using System.IO; +using System.IO.Compression; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Graphics.Texture.FileFormats +{ + public static class PngFileFormat + { + private const int ChunkOverheadSize = 12; + private const int MaxIdatChunkSize = 0x2000; + + private static readonly uint[] _crcTable; + + static PngFileFormat() + { + _crcTable = new uint[256]; + + uint c; + + for (int n = 0; n < _crcTable.Length; n++) + { + c = (uint)n; + + for (int k = 0; k < 8; k++) + { + if ((c & 1) != 0) + { + c = 0xedb88320 ^ (c >> 1); + } + else + { + c >>= 1; + } + } + + _crcTable[n] = c; + } + } + + private ref struct PngChunk + { + public uint ChunkType; + public ReadOnlySpan Data; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct PngHeader + { + public int Width; + public int Height; + public byte BitDepth; + public byte ColorType; + public byte CompressionMethod; + public byte FilterMethod; + public byte InterlaceMethod; + } + + private enum FilterType : byte + { + None = 0, + Sub = 1, + Up = 2, + Average = 3, + Paeth = 4, + } + + private const uint IhdrMagic = ((byte)'I' << 24) | ((byte)'H' << 16) | ((byte)'D' << 8) | (byte)'R'; + private const uint PlteMagic = ((byte)'P' << 24) | ((byte)'L' << 16) | ((byte)'T' << 8) | (byte)'E'; + private const uint IdatMagic = ((byte)'I' << 24) | ((byte)'D' << 16) | ((byte)'A' << 8) | (byte)'T'; + private const uint IendMagic = ((byte)'I' << 24) | ((byte)'E' << 16) | ((byte)'N' << 8) | (byte)'D'; + + private static readonly byte[] _pngSignature = new byte[] + { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, + }; + + public static ImageLoadResult TryLoadHeader(ReadOnlySpan pngData, out ImageParameters parameters) + { + parameters = default; + + if (pngData.Length < 8) + { + return ImageLoadResult.DataTooShort; + } + + if (!pngData[..8].SequenceEqual(_pngSignature)) + { + return ImageLoadResult.CorruptedHeader; + } + + pngData = pngData[8..]; + + ImageLoadResult result = TryParseChunk(pngData, out PngChunk ihdrChunk); + + if (result != ImageLoadResult.Success) + { + return result; + } + + if (ihdrChunk.ChunkType != IhdrMagic) + { + return ImageLoadResult.CorruptedHeader; + } + + if (ihdrChunk.Data.Length < Unsafe.SizeOf()) + { + return ImageLoadResult.DataTooShort; + } + + PngHeader header = MemoryMarshal.Cast(ihdrChunk.Data)[0]; + + if (!ValidateHeader(header)) + { + return ImageLoadResult.CorruptedHeader; + } + + parameters = new( + ReverseEndianness(header.Width), + ReverseEndianness(header.Height), + 1, + 1, + ImageFormat.R8G8B8A8Unorm, + ImageDimensions.Dim2D); + + return ImageLoadResult.Success; + } + + public static ImageLoadResult TryLoadData(ReadOnlySpan pngData, Span output) + { + if (pngData.Length < 8) + { + return ImageLoadResult.DataTooShort; + } + + if (!pngData[..8].SequenceEqual(_pngSignature)) + { + return ImageLoadResult.CorruptedHeader; + } + + pngData = pngData[8..]; + + ImageLoadResult result = TryParseChunk(pngData, out PngChunk ihdrChunk); + + if (result != ImageLoadResult.Success) + { + return result; + } + + if (ihdrChunk.ChunkType != IhdrMagic) + { + return ImageLoadResult.CorruptedHeader; + } + + if (ihdrChunk.Data.Length < Unsafe.SizeOf()) + { + return ImageLoadResult.DataTooShort; + } + + PngHeader header = MemoryMarshal.Cast(ihdrChunk.Data)[0]; + + if (!ValidateHeader(header)) + { + return ImageLoadResult.CorruptedHeader; + } + + // We currently don't support Adam7 interlaced images. + if (header.InterlaceMethod != 0) + { + return ImageLoadResult.UnsupportedFormat; + } + + // Make sure the output can fit the data. + if (output.Length < ReverseEndianness(header.Width) * ReverseEndianness(header.Height) * 4) + { + return ImageLoadResult.OutputTooShort; + } + + pngData = pngData[(ihdrChunk.Data.Length + ChunkOverheadSize)..]; + + int outputOffset = 0; + int bpp = header.ColorType switch + { + 0 => (header.BitDepth + 7) / 8, + 2 => ((header.BitDepth + 7) / 8) * 3, + 3 => 1, + 4 => ((header.BitDepth + 7) / 8) * 2, + 6 => ((header.BitDepth + 7) / 8) * 4, + _ => 0, + }; + + ReadOnlySpan palette = ReadOnlySpan.Empty; + + using MemoryStream compressedStream = new(); + using ZLibStream zLibStream = new(compressedStream, CompressionMode.Decompress); + + int stride = ReverseEndianness(header.Width) * bpp; + Span tempOutput = header.ColorType == 6 && header.BitDepth <= 8 ? output : new byte[stride * ReverseEndianness(header.Height)]; + byte[] scanline = new byte[stride]; + int scanlineOffset = 0; + int filterType = -1; + + while (pngData.Length > 0) + { + result = TryParseChunk(pngData, out PngChunk chunk); + + if (result != ImageLoadResult.Success) + { + return result; + } + + switch (chunk.ChunkType) + { + case IhdrMagic: + break; + case PlteMagic: + palette = DecodePalette(chunk.Data); + break; + case IdatMagic: + long position = compressedStream.Position; + compressedStream.Seek(0, SeekOrigin.End); + compressedStream.Write(chunk.Data); + compressedStream.Seek(position, SeekOrigin.Begin); + try + { + DecodeImageData( + zLibStream, + tempOutput, + ref outputOffset, + scanline, + ref scanlineOffset, + ref filterType, + ReverseEndianness(header.Width), + bpp); + } + catch (InvalidDataException) + { + return ImageLoadResult.CorruptedData; + } + break; + case IendMagic: + pngData = ReadOnlySpan.Empty; + break; + default: + bool isAncillary = char.IsAsciiLetterLower((char)(chunk.ChunkType >> 24)); + if (!isAncillary) + { + return ImageLoadResult.CorruptedHeader; + } + break; + } + + if (pngData.IsEmpty) + { + break; + } + + pngData = pngData[(chunk.Data.Length + ChunkOverheadSize)..]; + } + + if (header.BitDepth == 16) + { + Convert16BitTo8Bit(tempOutput[..(tempOutput.Length / 2)], tempOutput); + tempOutput = tempOutput[..(tempOutput.Length / 2)]; + } + + switch (header.ColorType) + { + case 0: + CopyLToRgba(output, tempOutput); + break; + case 2: + CopyRgbToRgba(output, tempOutput); + break; + case 3: + CopyIndexedToRgba(output, tempOutput, palette); + break; + case 4: + CopyLaToRgba(output, tempOutput); + break; + case 6: + if (header.BitDepth == 16) + { + tempOutput.CopyTo(output); + } + break; + } + + return ImageLoadResult.Success; + } + + private static bool ValidateHeader(in PngHeader header) + { + // Width and height must be a non-zero positive value. + if (ReverseEndianness(header.Width) <= 0 || ReverseEndianness(header.Height) <= 0) + { + return false; + } + + // Only compression and filter methods 0 were ever defined as part of the spec, everything else is invalid. + if ((header.CompressionMethod | header.FilterMethod) != 0) + { + return false; + } + + // Only interlace methods 0 (None) and 1 (Adam7) are valid. + if ((header.InterlaceMethod | 1) != 1) + { + return false; + } + + return header.ColorType switch + { + 0 => header.BitDepth == 1 || + header.BitDepth == 2 || + header.BitDepth == 4 || + header.BitDepth == 8 || + header.BitDepth == 16, + 2 or 4 or 6 => header.BitDepth == 8 || header.BitDepth == 16, + 3 => header.BitDepth == 1 || + header.BitDepth == 2 || + header.BitDepth == 4 || + header.BitDepth == 8, + _ => false, + }; + } + + private static ImageLoadResult TryParseChunk(ReadOnlySpan pngData, out PngChunk chunk) + { + if (pngData.Length < 8) + { + chunk = default; + return ImageLoadResult.DataTooShort; + } + + uint length = BinaryPrimitives.ReadUInt32BigEndian(pngData); + uint chunkType = BinaryPrimitives.ReadUInt32BigEndian(pngData[4..]); + + if (length + ChunkOverheadSize > pngData.Length) + { + chunk = default; + return ImageLoadResult.DataTooShort; + } + + uint crc = BinaryPrimitives.ReadUInt32BigEndian(pngData[(8 + (int)length)..]); + + ReadOnlySpan data = pngData.Slice(8, (int)length); + + if (crc != ComputeCrc(chunkType, data)) + { + chunk = default; + return ImageLoadResult.CorruptedData; + } + + chunk = new() + { + ChunkType = chunkType, + Data = data, + }; + + return ImageLoadResult.Success; + } + + private static uint[] DecodePalette(ReadOnlySpan input) + { + uint[] palette = new uint[input.Length / 3]; + + for (int i = 0; i < palette.Length; i++) + { + byte r = input[i * 3]; + byte g = input[i * 3 + 1]; + byte b = input[i * 3 + 2]; + + palette[i] = 0xff000000 | ((uint)b << 16) | ((uint)g << 8) | r; + + if (!BitConverter.IsLittleEndian) + { + palette[i] = BinaryPrimitives.ReverseEndianness(palette[i]); + } + } + + return palette; + } + + private static void DecodeImageData( + Stream zLibStream, + Span output, + ref int outputOffset, + byte[] scanline, + ref int scanlineOffset, + ref int filterType, + int width, + int bpp) + { + int stride = width * bpp; + + while (true) + { + if (filterType == -1) + { + filterType = zLibStream.ReadByte(); + + if (filterType == -1) + { + break; + } + } + + while (scanlineOffset < scanline.Length) + { + int bytesRead = zLibStream.Read(scanline.AsSpan()[scanlineOffset..]); + + if (bytesRead == 0) + { + return; + } + + scanlineOffset += bytesRead; + + if (scanlineOffset >= scanline.Length) + { + scanlineOffset = 0; + break; + } + } + + if (scanlineOffset == 0) + { + switch ((FilterType)filterType) + { + case FilterType.None: + scanline.AsSpan().CopyTo(output[outputOffset..]); + break; + case FilterType.Sub: + for (int x = 0; x < scanline.Length; x++) + { + byte left = x < bpp ? (byte)0 : output[outputOffset + x - bpp]; + output[outputOffset + x] = (byte)(left + scanline[x]); + } + break; + case FilterType.Up: + for (int x = 0; x < scanline.Length; x++) + { + byte above = outputOffset < stride ? (byte)0 : output[outputOffset + x - stride]; + output[outputOffset + x] = (byte)(above + scanline[x]); + } + break; + case FilterType.Average: + for (int x = 0; x < scanline.Length; x++) + { + byte left = x < bpp ? (byte)0 : output[outputOffset + x - bpp]; + byte above = outputOffset < stride ? (byte)0 : output[outputOffset + x - stride]; + output[outputOffset + x] = (byte)(((left + above) >> 1) + scanline[x]); + } + break; + case FilterType.Paeth: + for (int x = 0; x < scanline.Length; x++) + { + byte left = x < bpp ? (byte)0 : output[outputOffset + x - bpp]; + byte above = outputOffset < stride ? (byte)0 : output[outputOffset + x - stride]; + byte leftAbove = outputOffset < stride || x < bpp ? (byte)0 : output[outputOffset + x - bpp - stride]; + output[outputOffset + x] = (byte)(PaethPredict(left, above, leftAbove) + scanline[x]); + } + break; + } + + outputOffset += scanline.Length; + filterType = -1; + } + } + } + + public static void Save(Stream output, ImageParameters parameters, ReadOnlySpan data, bool fastMode = false) + { + output.Write(_pngSignature); + + WriteChunk(output, IhdrMagic, new PngHeader() + { + Width = ReverseEndianness(parameters.Width), + Height = ReverseEndianness(parameters.Height), + BitDepth = 8, + ColorType = 6, + }); + + byte[] encoded = EncodeImageData(data, parameters.Width, parameters.Height, fastMode); + + for (int encodedOffset = 0; encodedOffset < encoded.Length; encodedOffset += MaxIdatChunkSize) + { + int length = Math.Min(MaxIdatChunkSize, encoded.Length - encodedOffset); + + WriteChunk(output, IdatMagic, encoded.AsSpan().Slice(encodedOffset, length)); + } + + WriteChunk(output, IendMagic, ReadOnlySpan.Empty); + } + + private static byte[] EncodeImageData(ReadOnlySpan input, int width, int height, bool fastMode) + { + int bpp = 4; + int stride = width * bpp; + byte[] tempLine = new byte[stride]; + + using MemoryStream ms = new(); + + using (ZLibStream zLibStream = new(ms, fastMode ? CompressionLevel.Fastest : CompressionLevel.SmallestSize)) + { + for (int y = 0; y < height; y++) + { + ReadOnlySpan scanline = input.Slice(y * stride, stride); + FilterType filterType = fastMode ? FilterType.None : SelectFilterType(input, scanline, y, width, bpp); + + zLibStream.WriteByte((byte)filterType); + + switch (filterType) + { + case FilterType.None: + zLibStream.Write(scanline); + break; + case FilterType.Sub: + for (int x = 0; x < scanline.Length; x++) + { + byte left = x < bpp ? (byte)0 : scanline[x - bpp]; + tempLine[x] = (byte)(scanline[x] - left); + } + zLibStream.Write(tempLine); + break; + case FilterType.Up: + for (int x = 0; x < scanline.Length; x++) + { + byte above = y == 0 ? (byte)0 : input[y * stride + x - stride]; + tempLine[x] = (byte)(scanline[x] - above); + } + zLibStream.Write(tempLine); + break; + case FilterType.Average: + for (int x = 0; x < scanline.Length; x++) + { + byte left = x < bpp ? (byte)0 : scanline[x - bpp]; + byte above = y == 0 ? (byte)0 : input[y * stride + x - stride]; + tempLine[x] = (byte)(scanline[x] - ((left + above) >> 1)); + } + zLibStream.Write(tempLine); + break; + case FilterType.Paeth: + for (int x = 0; x < scanline.Length; x++) + { + byte left = x < bpp ? (byte)0 : scanline[x - bpp]; + byte above = y == 0 ? (byte)0 : input[y * stride + x - stride]; + byte leftAbove = y == 0 || x < bpp ? (byte)0 : input[y * stride + x - bpp - stride]; + tempLine[x] = (byte)(scanline[x] - PaethPredict(left, above, leftAbove)); + } + zLibStream.Write(tempLine); + break; + } + + } + } + + return ms.ToArray(); + } + + private static FilterType SelectFilterType(ReadOnlySpan input, ReadOnlySpan scanline, int y, int width, int bpp) + { + int stride = width * bpp; + + Span deltas = stackalloc int[4]; + + for (int x = 0; x < scanline.Length; x++) + { + byte left = x < bpp ? (byte)0 : scanline[x - bpp]; + byte above = y == 0 ? (byte)0 : input[y * stride + x - stride]; + byte leftAbove = y == 0 || x < bpp ? (byte)0 : input[y * stride + x - bpp - stride]; + + int value = scanline[x]; + int valueSub = value - left; + int valueUp = value - above; + int valueAverage = value - ((left + above) >> 1); + int valuePaeth = value - PaethPredict(left, above, leftAbove); + + deltas[0] += Math.Abs(valueSub); + deltas[1] += Math.Abs(valueUp); + deltas[2] += Math.Abs(valueAverage); + deltas[3] += Math.Abs(valuePaeth); + } + + int lowestDelta = int.MaxValue; + FilterType bestCandidate = FilterType.None; + + for (int i = 0; i < deltas.Length; i++) + { + if (deltas[i] < lowestDelta) + { + lowestDelta = deltas[i]; + bestCandidate = (FilterType)(i + 1); + } + } + + return bestCandidate; + } + + private static void WriteChunk(Stream output, uint chunkType, T data) where T : unmanaged + { + WriteChunk(output, chunkType, MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref data, 1))); + } + + private static void WriteChunk(Stream output, uint chunkType, ReadOnlySpan data) + { + WriteUInt32BE(output, (uint)data.Length); + WriteUInt32BE(output, chunkType); + output.Write(data); + WriteUInt32BE(output, ComputeCrc(chunkType, data)); + } + + private static void WriteUInt32BE(Stream output, uint value) + { + output.WriteByte((byte)(value >> 24)); + output.WriteByte((byte)(value >> 16)); + output.WriteByte((byte)(value >> 8)); + output.WriteByte((byte)value); + } + + private static int PaethPredict(int a, int b, int c) + { + int p = a + b - c; + int pa = Math.Abs(p - a); + int pb = Math.Abs(p - b); + int pc = Math.Abs(p - c); + + if (pa <= pb && pa <= pc) + { + return a; + } + else if (pb <= pc) + { + return b; + } + else + { + return c; + } + } + + private static void Convert16BitTo8Bit(Span output, ReadOnlySpan input) + { + for (int i = 0; i < input.Length; i += 2) + { + output[i / 2] = input[i]; + } + } + + private static void CopyLToRgba(Span output, ReadOnlySpan input) + { + int width = input.Length; + + for (int pixel = 0; pixel < width; pixel++) + { + byte luminance = input[pixel]; + int dstX = pixel * 4; + + output[dstX] = luminance; + output[dstX + 1] = luminance; + output[dstX + 2] = luminance; + output[dstX + 3] = 0xff; + } + } + + private static void CopyRgbToRgba(Span output, ReadOnlySpan input) + { + int width = input.Length / 3; + + for (int pixel = 0; pixel < width; pixel++) + { + int srcX = pixel * 3; + int dstX = pixel * 4; + + output[dstX] = input[srcX]; + output[dstX + 1] = input[srcX + 1]; + output[dstX + 2] = input[srcX + 2]; + output[dstX + 3] = 0xff; + } + } + + private static void CopyIndexedToRgba(Span output, ReadOnlySpan input, ReadOnlySpan palette) + { + Span outputAsUint = MemoryMarshal.Cast(output); + + for (int pixel = 0; pixel < outputAsUint.Length; pixel++) + { + byte index = input[pixel]; + + if (index < palette.Length) + { + outputAsUint[pixel] = palette[index]; + } + } + } + + private static void CopyLaToRgba(Span output, ReadOnlySpan input) + { + int width = input.Length / 2; + + for (int pixel = 0; pixel < width; pixel++) + { + int srcX = pixel * 2; + int dstX = pixel * 4; + + byte luminance = input[srcX]; + byte alpha = input[srcX + 1]; + + output[dstX] = luminance; + output[dstX + 1] = luminance; + output[dstX + 2] = luminance; + output[dstX + 3] = alpha; + } + } + + private static uint ComputeCrc(uint chunkType, ReadOnlySpan input) + { + uint crc = UpdateCrc(uint.MaxValue, (byte)(chunkType >> 24)); + crc = UpdateCrc(crc, (byte)(chunkType >> 16)); + crc = UpdateCrc(crc, (byte)(chunkType >> 8)); + crc = UpdateCrc(crc, (byte)chunkType); + crc = UpdateCrc(crc, input); + + return ~crc; + } + + private static uint UpdateCrc(uint crc, byte input) + { + return _crcTable[(byte)(crc ^ input)] ^ (crc >> 8); + } + + private static uint UpdateCrc(uint crc, ReadOnlySpan input) + { + uint c = crc; + + for (int n = 0; n < input.Length; n++) + { + c = _crcTable[(byte)(c ^ input[n])] ^ (c >> 8); + } + + return c; + } + + private static int ReverseEndianness(int value) + { + if (BitConverter.IsLittleEndian) + { + return BinaryPrimitives.ReverseEndianness(value); + } + + return value; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index ee179c929..9b6a832f0 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -8,6 +8,7 @@ using LibHac.Tools.FsSystem.RomFs; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; +using Ryujinx.Graphics.Gpu; using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Mods; @@ -28,6 +29,7 @@ namespace Ryujinx.HLE.HOS private const string RomfsDir = "romfs"; private const string ExefsDir = "exefs"; private const string CheatDir = "cheats"; + private const string TexturesDir = "textures"; private const string RomfsContainer = "romfs.bin"; private const string ExefsContainer = "exefs.nsp"; private const string StubExtension = ".stub"; @@ -81,6 +83,7 @@ namespace Ryujinx.HLE.HOS public List> RomfsDirs { get; } public List> ExefsDirs { get; } + public List> TextureDirs { get; } public List Cheats { get; } @@ -90,6 +93,7 @@ namespace Ryujinx.HLE.HOS ExefsContainers = new List>(); RomfsDirs = new List>(); ExefsDirs = new List>(); + TextureDirs = new List>(); Cheats = new List(); } } @@ -187,6 +191,14 @@ namespace Ryujinx.HLE.HOS mods.ExefsDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); types.Append('E'); } + else if (StrEquals(TexturesDir, modDir.Name)) + { + var modData = modMetadata.Mods.Find(x => modDir.FullName.Contains(x.Path)); + var enabled = modData?.Enabled ?? true; + + mods.TextureDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); + types.Append('T'); + } else if (StrEquals(CheatDir, modDir.Name)) { types.Append('C', QueryCheatsDir(mods, modDir)); @@ -699,6 +711,23 @@ namespace Ryujinx.HLE.HOS return ApplyProgramPatches(nsoMods, 0x100, programs); } + internal void ApplyTextureMods(ulong applicationId, GpuContext gpuContext) + { + if (!_appMods.TryGetValue(applicationId, out ModCache mods) || mods.TextureDirs.Count == 0) + { + return; + } + + var textureMods = mods.TextureDirs; + + foreach (var mod in textureMods) + { + gpuContext.DiskTextureStorage.AddInputDirectory(mod.Path.FullName); + + Logger.Info?.Print(LogClass.ModLoader, $"Found texture replacements on mod '{mod.Name}'"); + } + } + internal void LoadCheats(ulong applicationId, ProcessTamperInfo tamperInfo, TamperMachine tamperMachine) { if (tamperInfo?.BuildIds == null || tamperInfo.CodeAddresses == null) diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs index 3904d660e..0d8c488af 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs @@ -84,7 +84,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions // Don't use PTC if ExeFS files have been replaced. bool enablePtc = device.System.EnablePtc && !modLoadResult.Modified; - if (!enablePtc) + if (modLoadResult.Modified) { Logger.Warning?.Print(LogClass.Ptc, "Detected unsupported ExeFs modifications. PTC disabled."); } @@ -105,6 +105,9 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions Graphics.Gpu.GraphicsConfig.TitleId = $"{programId:x16}"; device.Gpu.HostInitalized.Set(); + // Load texture replacements. + device.Configuration.VirtualFileSystem.ModLoader.ApplyTextureMods(programId, device.Gpu); + if (!MemoryBlock.SupportsFlags(MemoryAllocationFlags.ViewCompatible)) { device.Configuration.MemoryManagerMode = MemoryManagerMode.SoftwarePageTable; diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs index af3ad0a1d..55ac59ce9 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs @@ -15,7 +15,7 @@ namespace Ryujinx.UI.Common.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 51; + public const int CurrentVersion = 52; /// /// Version of the configuration file format @@ -68,10 +68,30 @@ namespace Ryujinx.UI.Common.Configuration public int ScalingFilterLevel { get; set; } /// - /// Dumps shaders in this local directory + /// Directory to save the game shaders. /// public string GraphicsShadersDumpPath { get; set; } + /// + /// Directory to save the game textures, if texture dumping is enabled. + /// + public string GraphicsTexturesDumpPath { get; set; } + + /// + /// File format used to dump textures. + /// + public TextureFileFormat GraphicsTexturesDumpFileFormat { get; set; } + + /// + /// Enables texture dumping. + /// + public bool GraphicsEnableTextureDump { get; set; } + + /// + /// Enables real-time texture editing. + /// + public bool GraphicsEnableTextureRealTimeEdit { get; set; } + /// /// Enables printing debug log messages /// diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index 8420dc5d9..a8333fabe 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -464,10 +464,30 @@ namespace Ryujinx.UI.Common.Configuration public ReactiveObject ResScaleCustom { get; private set; } /// - /// Dumps shaders in this local directory + /// Directory to save the game shaders. /// public ReactiveObject ShadersDumpPath { get; private set; } + /// + /// Directory to save the game textures, if texture dumping is enabled. + /// + public ReactiveObject TexturesDumpPath { get; private set; } + + /// + /// File format used to dump textures. + /// + public ReactiveObject TexturesDumpFileFormat { get; private set; } + + /// + /// Enables texture dumping. + /// + public ReactiveObject EnableTextureDump { get; private set; } + + /// + /// Enables real-time texture editing. + /// + public ReactiveObject EnableTextureRealTimeEdit { get; private set; } + /// /// Enables or disables Vertical Sync /// @@ -531,6 +551,13 @@ namespace Ryujinx.UI.Common.Configuration AspectRatio = new ReactiveObject(); AspectRatio.Event += static (sender, e) => LogValueChange(e, nameof(AspectRatio)); ShadersDumpPath = new ReactiveObject(); + TexturesDumpPath = new ReactiveObject(); + TexturesDumpFileFormat = new ReactiveObject(); + TexturesDumpFileFormat.Event += static (sender, e) => LogValueChange(e, nameof(TexturesDumpFileFormat)); + EnableTextureDump = new ReactiveObject(); + EnableTextureDump.Event += static (sender, e) => LogValueChange(e, nameof(EnableTextureDump)); + EnableTextureRealTimeEdit = new ReactiveObject(); + EnableTextureRealTimeEdit.Event += static (sender, e) => LogValueChange(e, nameof(EnableTextureRealTimeEdit)); EnableVsync = new ReactiveObject(); EnableVsync.Event += static (sender, e) => LogValueChange(e, nameof(EnableVsync)); EnableShaderCache = new ReactiveObject(); @@ -673,6 +700,10 @@ namespace Ryujinx.UI.Common.Configuration ScalingFilter = Graphics.ScalingFilter, ScalingFilterLevel = Graphics.ScalingFilterLevel, GraphicsShadersDumpPath = Graphics.ShadersDumpPath, + GraphicsTexturesDumpPath = Graphics.TexturesDumpPath, + GraphicsTexturesDumpFileFormat = Graphics.TexturesDumpFileFormat, + GraphicsEnableTextureDump = Graphics.EnableTextureDump, + GraphicsEnableTextureRealTimeEdit = Graphics.EnableTextureRealTimeEdit, LoggingEnableDebug = Logger.EnableDebug, LoggingEnableStub = Logger.EnableStub, LoggingEnableInfo = Logger.EnableInfo, @@ -782,6 +813,10 @@ namespace Ryujinx.UI.Common.Configuration Graphics.GraphicsBackend.Value = DefaultGraphicsBackend(); Graphics.PreferredGpu.Value = ""; Graphics.ShadersDumpPath.Value = ""; + Graphics.TexturesDumpPath.Value = ""; + Graphics.TexturesDumpFileFormat.Value = TextureFileFormat.Dds; + Graphics.EnableTextureDump.Value = true; + Graphics.EnableTextureRealTimeEdit.Value = true; Logger.EnableDebug.Value = false; Logger.EnableStub.Value = true; Logger.EnableInfo.Value = true; @@ -1477,12 +1512,28 @@ namespace Ryujinx.UI.Common.Configuration configurationFileUpdated = true; } + if (configurationFileFormat.Version < 52) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 52."); + + configurationFileFormat.GraphicsTexturesDumpPath = ""; + configurationFileFormat.GraphicsTexturesDumpFileFormat = TextureFileFormat.Dds; + configurationFileFormat.GraphicsEnableTextureDump = true; + configurationFileFormat.GraphicsEnableTextureRealTimeEdit = true; + + configurationFileUpdated = true; + } + Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; Graphics.ResScale.Value = configurationFileFormat.ResScale; Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; Graphics.MaxAnisotropy.Value = configurationFileFormat.MaxAnisotropy; Graphics.AspectRatio.Value = configurationFileFormat.AspectRatio; Graphics.ShadersDumpPath.Value = configurationFileFormat.GraphicsShadersDumpPath; + Graphics.TexturesDumpPath.Value = configurationFileFormat.GraphicsTexturesDumpPath; + Graphics.TexturesDumpFileFormat.Value = configurationFileFormat.GraphicsTexturesDumpFileFormat; + Graphics.EnableTextureDump.Value = configurationFileFormat.GraphicsEnableTextureDump; + Graphics.EnableTextureRealTimeEdit.Value = configurationFileFormat.GraphicsEnableTextureRealTimeEdit; Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading; Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend; Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu; diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 74e18056b..2dba316e1 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -171,6 +171,12 @@ "SettingsTabGraphicsAspectRatioStretch": "Stretch to Fit Window", "SettingsTabGraphicsDeveloperOptions": "Developer Options", "SettingsTabGraphicsShaderDumpPath": "Graphics Shader Dump Path:", + "SettingsTabGraphicsTextureDumpPath": "Textures Dump Path:", + "SettingsTabGraphicsTextureDumpFormat": "Textures Dump File Format:", + "SettingsTabGraphicsTextureDumpFormatDds": "DDS (DirectDraw Surface)", + "SettingsTabGraphicsTextureDumpFormatPng": "PNG (Portable Network Graphics)", + "SettingsTabGraphicsEnableTextureDump": "Enable Texture Dump", + "SettingsTabGraphicsEnableTextureRealTimeEditing": "Enable Real Time Texture Editing", "SettingsTabLogging": "Logging", "SettingsTabLoggingLogging": "Logging", "SettingsTabLoggingEnableLoggingToFile": "Enable Logging to File", @@ -585,6 +591,10 @@ "AnisotropyTooltip": "Level of Anisotropic Filtering. Set to Auto to use the value requested by the game.", "AspectRatioTooltip": "Aspect Ratio applied to the renderer window.\n\nOnly change this if you're using an aspect ratio mod for your game, otherwise the graphics will be stretched.\n\nLeave on 16:9 if unsure.", "ShaderDumpPathTooltip": "Graphics Shaders Dump Path", + "TextureDumpPathTooltip": "Optional folder where all the game textures will be saved", + "GraphicsTextureDumpFormatTooltip": "File format that will be used to save the game textures, if texture dumping is enabled", + "GraphicsEnableTextureDumpTooltip": "Enables saving all game textures to the specified folder", + "GraphicsEnableTextureRealTimeEditingTooltip": "Enables applying changes to dumped textures into the game as the files are edited, in real time", "FileLogTooltip": "Saves console logging to a log file on disk. Does not affect performance.", "StubLogTooltip": "Prints stub log messages in the console. Does not affect performance.", "InfoLogTooltip": "Prints info log messages in the console. Does not affect performance.", diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 70e5fa5c7..ba1ec4c82 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -168,6 +168,11 @@ namespace Ryujinx.Ava.UI.ViewModels public string TimeZone { get; set; } public string ShaderDumpPath { get; set; } + public string TextureDumpPath { get; set; } + public int TextureDumpFormatIndex { get; set; } + public bool EnableTextureDump { get; set; } + public bool EnableTextureRealTimeEditing { get; set; } + public int Language { get; set; } public int Region { get; set; } public int FsGlobalAccessLogMode { get; set; } @@ -447,6 +452,10 @@ namespace Ryujinx.Ava.UI.ViewModels AspectRatio = (int)config.Graphics.AspectRatio.Value; GraphicsBackendMultithreadingIndex = (int)config.Graphics.BackendThreading.Value; ShaderDumpPath = config.Graphics.ShadersDumpPath; + TextureDumpPath = config.Graphics.TexturesDumpPath.Value; + TextureDumpFormatIndex = (int)config.Graphics.TexturesDumpFileFormat.Value; + EnableTextureDump = config.Graphics.EnableTextureDump.Value; + EnableTextureRealTimeEditing = config.Graphics.EnableTextureRealTimeEdit.Value; AntiAliasingEffect = (int)config.Graphics.AntiAliasing.Value; ScalingFilter = (int)config.Graphics.ScalingFilter.Value; ScalingFilterLevel = config.Graphics.ScalingFilterLevel.Value; @@ -550,6 +559,10 @@ namespace Ryujinx.Ava.UI.ViewModels config.Graphics.BackendThreading.Value = (BackendThreading)GraphicsBackendMultithreadingIndex; config.Graphics.ShadersDumpPath.Value = ShaderDumpPath; + config.Graphics.TexturesDumpPath.Value = TextureDumpPath; + config.Graphics.TexturesDumpFileFormat.Value = (TextureFileFormat)TextureDumpFormatIndex; + config.Graphics.EnableTextureDump.Value = EnableTextureDump; + config.Graphics.EnableTextureRealTimeEdit.Value = EnableTextureRealTimeEditing; // Audio AudioBackend audioBackend = (AudioBackend)AudioBackend; diff --git a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml index 5cffc6848..14f0f247e 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml @@ -295,6 +295,48 @@ ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}" /> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 348412e78..b69411ca6 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -12,6 +12,7 @@ using Ryujinx.Ava.Input; using Ryujinx.Ava.UI.Applet; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Graphics.Gpu; using Ryujinx.HLE.FileSystem; @@ -512,6 +513,10 @@ namespace Ryujinx.Ava.UI.Windows GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache; GraphicsConfig.EnableTextureRecompression = ConfigurationState.Instance.Graphics.EnableTextureRecompression; GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE; + GraphicsConfig.TextureDumpPath = ConfigurationState.Instance.Graphics.TexturesDumpPath; + GraphicsConfig.TextureDumpFormatPng = ConfigurationState.Instance.Graphics.TexturesDumpFileFormat == TextureFileFormat.Png; + GraphicsConfig.EnableTextureDump = ConfigurationState.Instance.Graphics.EnableTextureDump; + GraphicsConfig.EnableTextureRealTimeEdit = ConfigurationState.Instance.Graphics.EnableTextureRealTimeEdit; #pragma warning restore IDE0055 }