Initial texture replacement implementation
This commit is contained in:
parent
2c5c0392f9
commit
bab9477656
26 changed files with 3510 additions and 68 deletions
12
src/Ryujinx.Common/Configuration/TextureFileFormat.cs
Normal file
12
src/Ryujinx.Common/Configuration/TextureFileFormat.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<TextureFileFormat>))]
|
||||
public enum TextureFileFormat
|
||||
{
|
||||
Dds,
|
||||
Png,
|
||||
}
|
||||
}
|
|
@ -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
|
|||
/// </summary>
|
||||
public Window Window { get; }
|
||||
|
||||
public DiskTextureStorage DiskTextureStorage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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();
|
||||
}
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ namespace Ryujinx.Graphics.Gpu
|
|||
|
||||
/// <summary>
|
||||
/// Title id of the current running game.
|
||||
/// Used by the shader cache.
|
||||
/// Used by the shader cache and texture dumping.
|
||||
/// </summary>
|
||||
public static string TitleId;
|
||||
|
||||
|
@ -72,6 +72,26 @@ namespace Ryujinx.Graphics.Gpu
|
|||
/// Enables or disables color space passthrough, if available.
|
||||
/// </summary>
|
||||
public static bool EnableColorSpacePassthrough = false;
|
||||
|
||||
/// <summary>
|
||||
/// Base directory used to write the game textures, if texture dump is enabled.
|
||||
/// </summary>
|
||||
public static string TextureDumpPath;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if textures should be saved using the PNG file format. If disabled, textures are saved as DDS.
|
||||
/// </summary>
|
||||
public static bool TextureDumpFormatPng;
|
||||
|
||||
/// <summary>
|
||||
/// Enables dumping textures to file.
|
||||
/// </summary>
|
||||
public static bool EnableTextureDump;
|
||||
|
||||
/// <summary>
|
||||
/// Monitors dumped texture files for change and applies them in real-time if enabled.
|
||||
/// </summary>
|
||||
public static bool EnableTextureRealTimeEdit;
|
||||
}
|
||||
#pragma warning restore CA2211
|
||||
}
|
||||
|
|
1141
src/Ryujinx.Graphics.Gpu/Image/DiskTextureStorage.cs
Normal file
1141
src/Ryujinx.Graphics.Gpu/Image/DiskTextureStorage.cs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -65,6 +65,16 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||
/// </summary>
|
||||
public int Height { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Texture depth, or 1 if the texture is not a 3D texture.
|
||||
/// </summary>
|
||||
public int Depth { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Numer of texture layers, or 1 if the texture is not a array texture.
|
||||
/// </summary>
|
||||
public int Layers { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Texture information.
|
||||
/// </summary>
|
||||
|
@ -107,11 +117,13 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||
/// </summary>
|
||||
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
|
|||
/// <param name="firstLevel">The first mipmap level of the texture, or 0 if the texture has no parent</param>
|
||||
/// <param name="scaleFactor">The floating point scale factor to initialize with</param>
|
||||
/// <param name="scaleMode">The scale mode to initialize with</param>
|
||||
/// <param name="forRender">Indicates that the texture will be modified by a draw or blit operation</param>
|
||||
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
|
|||
/// <param name="sizeInfo">Size information of the texture</param>
|
||||
/// <param name="range">Physical memory ranges where the texture data is located</param>
|
||||
/// <param name="scaleMode">The scale mode to initialize with. If scaled, the texture's data is loaded immediately and scaled up</param>
|
||||
/// <param name="forRender">Indicates that the texture will be modified by a draw or blit operation</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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<byte> 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
|
|||
/// <returns>True if data was flushed, false otherwise</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1043,7 +1132,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||
/// <param name="tracked">Whether or not the flush triggers write tracking. If it doesn't, the texture will not be blacklisted for scaling either.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -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
|
|||
/// <param name="info">Texture information</param>
|
||||
/// <param name="caps">GPU capabilities</param>
|
||||
/// <param name="scale">Texture scale factor, to be applied to the texture size</param>
|
||||
/// <param name="infoOverride">Optional parameters to override the <paramref name="info"/> parameter</param>
|
||||
/// <returns>The texture creation information</returns>
|
||||
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,
|
||||
|
|
|
@ -50,10 +50,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||
/// <param name="info">Texture information</param>
|
||||
/// <param name="caps">Host GPU capabilities</param>
|
||||
/// <returns>True if the format is incompatible, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -64,10 +64,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||
/// This can be used to convert a incompatible compressed format to the decompressor
|
||||
/// output format.
|
||||
/// </remarks>
|
||||
/// <param name="info">Texture information</param>
|
||||
/// <param name="formatInfo">Texture format information</param>
|
||||
/// <param name="target">Texture dimensions</param>
|
||||
/// <param name="caps">Host GPU capabilities</param>
|
||||
/// <returns>A host compatible format</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -166,7 +167,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||
/// <param name="target">Target usage of the texture</param>
|
||||
/// <param name="caps">Host GPU Capabilities</param>
|
||||
/// <returns>True if the texture host supports the format with the given target usage, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a texture can flush its data back to guest memory.
|
||||
/// </summary>
|
||||
/// <param name="info">Texture that will have its data flushed</param>
|
||||
/// <param name="caps">Host GPU Capabilities</param>
|
||||
/// <returns>True if the texture can flush, false otherwise</returns>
|
||||
public static bool CanTextureFlush(Texture texture, in Capabilities caps)
|
||||
{
|
||||
return !texture.HasImportOverride() && CanTextureFlush(texture.Info, caps);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a texture can flush its data back to guest memory.
|
||||
/// </summary>
|
||||
/// <param name="info">Texture information</param>
|
||||
/// <param name="caps">Host GPU Capabilities</param>
|
||||
/// <returns>True if the texture can flush, false otherwise</returns>
|
||||
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.
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -207,6 +207,16 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||
return GetLayers(Target, DepthOrLayers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns>The number of texture layers or depth</returns>
|
||||
public int GetDepthOrLayers()
|
||||
{
|
||||
return GetDepthOrLayers(Target, DepthOrLayers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="target">Texture target</param>
|
||||
/// <param name="depthOrLayers">Texture layers if the is a array texture, depth for 3D textures, ignored otherwise</param>
|
||||
/// <returns>The number of texture layers or depth</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
|
|
67
src/Ryujinx.Graphics.Gpu/Image/TextureInfoOverride.cs
Normal file
67
src/Ryujinx.Graphics.Gpu/Image/TextureInfoOverride.cs
Normal file
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// Values that should override <see cref="TextureInfo"/> parameters.
|
||||
/// </summary>
|
||||
readonly struct TextureInfoOverride
|
||||
{
|
||||
/// <summary>
|
||||
/// Texture width override.
|
||||
/// </summary>
|
||||
public int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Texture height override.
|
||||
/// </summary>
|
||||
public int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Texture depth (for 3D textures), or layers count override.
|
||||
/// </summary>
|
||||
public int DepthOrLayers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Mipmap levels override.
|
||||
/// </summary>
|
||||
public int Levels { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Texture format override.
|
||||
/// </summary>
|
||||
public FormatInfo FormatInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs the texture override structure.
|
||||
/// </summary>
|
||||
/// <param name="width">Texture width override</param>
|
||||
/// <param name="height">Texture height override</param>
|
||||
/// <param name="depthOrLayers">Texture depth (for 3D textures), or layers count override</param>
|
||||
/// <param name="levels">Mipmap levels override</param>
|
||||
/// <param name="formatInfo">Texture format override</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
26
src/Ryujinx.Graphics.Gpu/Image/TextureInfoOverrideFlags.cs
Normal file
26
src/Ryujinx.Graphics.Gpu/Image/TextureInfoOverrideFlags.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// Flags controlling which parameters of the texture should be overriden.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
enum TextureInfoOverrideFlags
|
||||
{
|
||||
/// <summary>
|
||||
/// Nothing should be overriden.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The texture size (width, height, depth and levels) should be overriden.
|
||||
/// </summary>
|
||||
OverrideSize = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// The texture format should be overriden.
|
||||
/// </summary>
|
||||
OverrideFormat = 1 << 1
|
||||
}
|
||||
}
|
859
src/Ryujinx.Graphics.Texture/FileFormats/DdsFileFormat.cs
Normal file
859
src/Ryujinx.Graphics.Texture/FileFormats/DdsFileFormat.cs
Normal file
|
@ -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<uint> 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<byte> ddsData, out ImageParameters parameters)
|
||||
{
|
||||
return TryLoadHeaderImpl(ddsData, out parameters, out _);
|
||||
}
|
||||
|
||||
private static ImageLoadResult TryLoadHeaderImpl(ReadOnlySpan<byte> ddsData, out ImageParameters parameters, out int dataOffset)
|
||||
{
|
||||
parameters = default;
|
||||
dataOffset = 0;
|
||||
|
||||
if (ddsData.Length < 4 + Unsafe.SizeOf<DdsHeader>())
|
||||
{
|
||||
return ImageLoadResult.DataTooShort;
|
||||
}
|
||||
|
||||
uint magic = ddsData.Read<uint>();
|
||||
DdsHeader header = ddsData[4..].Read<DdsHeader>();
|
||||
|
||||
if (magic != DdsMagic ||
|
||||
header.Size != Unsafe.SizeOf<DdsHeader>() ||
|
||||
header.DdsPf.Size != Unsafe.SizeOf<DdsPixelFormat>())
|
||||
{
|
||||
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<DdsHeader>();
|
||||
|
||||
if (header.DdsPf.Flags.HasFlag(DdsPfFlags.FourCC) && header.DdsPf.FourCC == Dx10FourCC)
|
||||
{
|
||||
if (ddsData.Length < 4 + Unsafe.SizeOf<DdsHeader>() + Unsafe.SizeOf<DdsHeaderDxt10>())
|
||||
{
|
||||
return ImageLoadResult.DataTooShort;
|
||||
}
|
||||
|
||||
DdsHeaderDxt10 headerDxt10 = ddsData[dataOffset..].Read<DdsHeaderDxt10>();
|
||||
|
||||
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<DdsHeaderDxt10>();
|
||||
}
|
||||
|
||||
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<byte> ddsData, Span<byte> 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<byte> destination, ReadOnlySpan<byte> 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<byte> 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<DdsHeader>(),
|
||||
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<byte> 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<T>(this Stream stream, T value) where T : unmanaged
|
||||
{
|
||||
stream.Write(MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref value, 1)));
|
||||
}
|
||||
|
||||
private static T Read<T>(this ReadOnlySpan<byte> span) where T : unmanaged
|
||||
{
|
||||
return MemoryMarshal.Cast<byte, T>(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<DdsPixelFormat>(),
|
||||
Flags = DdsPfFlags.FourCC,
|
||||
FourCC = Dx10FourCC,
|
||||
};
|
||||
}
|
||||
|
||||
private static DdsPixelFormat CreatePixelFormat(ImageFormat format)
|
||||
{
|
||||
DdsPixelFormat pf = new()
|
||||
{
|
||||
Size = (uint)Unsafe.SizeOf<DdsPixelFormat>(),
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
125
src/Ryujinx.Graphics.Texture/FileFormats/DxgiFormat.cs
Normal file
125
src/Ryujinx.Graphics.Texture/FileFormats/DxgiFormat.cs
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
11
src/Ryujinx.Graphics.Texture/FileFormats/ImageDimensions.cs
Normal file
11
src/Ryujinx.Graphics.Texture/FileFormats/ImageDimensions.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
namespace Ryujinx.Graphics.Texture.FileFormats
|
||||
{
|
||||
public enum ImageDimensions
|
||||
{
|
||||
Dim2D,
|
||||
Dim2DArray,
|
||||
Dim3D,
|
||||
DimCube,
|
||||
DimCubeArray,
|
||||
}
|
||||
}
|
28
src/Ryujinx.Graphics.Texture/FileFormats/ImageFormat.cs
Normal file
28
src/Ryujinx.Graphics.Texture/FileFormats/ImageFormat.cs
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
12
src/Ryujinx.Graphics.Texture/FileFormats/ImageLoadResult.cs
Normal file
12
src/Ryujinx.Graphics.Texture/FileFormats/ImageLoadResult.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace Ryujinx.Graphics.Texture.FileFormats
|
||||
{
|
||||
public enum ImageLoadResult
|
||||
{
|
||||
Success,
|
||||
CorruptedHeader,
|
||||
CorruptedData,
|
||||
DataTooShort,
|
||||
OutputTooShort,
|
||||
UnsupportedFormat,
|
||||
}
|
||||
}
|
22
src/Ryujinx.Graphics.Texture/FileFormats/ImageParameters.cs
Normal file
22
src/Ryujinx.Graphics.Texture/FileFormats/ImageParameters.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
757
src/Ryujinx.Graphics.Texture/FileFormats/PngFileFormat.cs
Normal file
757
src/Ryujinx.Graphics.Texture/FileFormats/PngFileFormat.cs
Normal file
|
@ -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<byte> 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<byte> 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<PngHeader>())
|
||||
{
|
||||
return ImageLoadResult.DataTooShort;
|
||||
}
|
||||
|
||||
PngHeader header = MemoryMarshal.Cast<byte, PngHeader>(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<byte> pngData, Span<byte> 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<PngHeader>())
|
||||
{
|
||||
return ImageLoadResult.DataTooShort;
|
||||
}
|
||||
|
||||
PngHeader header = MemoryMarshal.Cast<byte, PngHeader>(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<uint> palette = ReadOnlySpan<uint>.Empty;
|
||||
|
||||
using MemoryStream compressedStream = new();
|
||||
using ZLibStream zLibStream = new(compressedStream, CompressionMode.Decompress);
|
||||
|
||||
int stride = ReverseEndianness(header.Width) * bpp;
|
||||
Span<byte> 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<byte>.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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte>.Empty);
|
||||
}
|
||||
|
||||
private static byte[] EncodeImageData(ReadOnlySpan<byte> 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<byte> 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<byte> input, ReadOnlySpan<byte> scanline, int y, int width, int bpp)
|
||||
{
|
||||
int stride = width * bpp;
|
||||
|
||||
Span<int> 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<T>(Stream output, uint chunkType, T data) where T : unmanaged
|
||||
{
|
||||
WriteChunk(output, chunkType, MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref data, 1)));
|
||||
}
|
||||
|
||||
private static void WriteChunk(Stream output, uint chunkType, ReadOnlySpan<byte> 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<byte> output, ReadOnlySpan<byte> input)
|
||||
{
|
||||
for (int i = 0; i < input.Length; i += 2)
|
||||
{
|
||||
output[i / 2] = input[i];
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyLToRgba(Span<byte> output, ReadOnlySpan<byte> 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<byte> output, ReadOnlySpan<byte> 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<byte> output, ReadOnlySpan<byte> input, ReadOnlySpan<uint> palette)
|
||||
{
|
||||
Span<uint> outputAsUint = MemoryMarshal.Cast<byte, uint>(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<byte> output, ReadOnlySpan<byte> 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<byte> 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<byte> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Mod<DirectoryInfo>> RomfsDirs { get; }
|
||||
public List<Mod<DirectoryInfo>> ExefsDirs { get; }
|
||||
public List<Mod<DirectoryInfo>> TextureDirs { get; }
|
||||
|
||||
public List<Cheat> Cheats { get; }
|
||||
|
||||
|
@ -90,6 +93,7 @@ namespace Ryujinx.HLE.HOS
|
|||
ExefsContainers = new List<Mod<FileInfo>>();
|
||||
RomfsDirs = new List<Mod<DirectoryInfo>>();
|
||||
ExefsDirs = new List<Mod<DirectoryInfo>>();
|
||||
TextureDirs = new List<Mod<DirectoryInfo>>();
|
||||
Cheats = new List<Cheat>();
|
||||
}
|
||||
}
|
||||
|
@ -187,6 +191,14 @@ namespace Ryujinx.HLE.HOS
|
|||
mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>(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<DirectoryInfo>(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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace Ryujinx.UI.Common.Configuration
|
|||
/// <summary>
|
||||
/// The current version of the file format
|
||||
/// </summary>
|
||||
public const int CurrentVersion = 51;
|
||||
public const int CurrentVersion = 52;
|
||||
|
||||
/// <summary>
|
||||
/// Version of the configuration file format
|
||||
|
@ -68,10 +68,30 @@ namespace Ryujinx.UI.Common.Configuration
|
|||
public int ScalingFilterLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Dumps shaders in this local directory
|
||||
/// Directory to save the game shaders.
|
||||
/// </summary>
|
||||
public string GraphicsShadersDumpPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Directory to save the game textures, if texture dumping is enabled.
|
||||
/// </summary>
|
||||
public string GraphicsTexturesDumpPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// File format used to dump textures.
|
||||
/// </summary>
|
||||
public TextureFileFormat GraphicsTexturesDumpFileFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables texture dumping.
|
||||
/// </summary>
|
||||
public bool GraphicsEnableTextureDump { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables real-time texture editing.
|
||||
/// </summary>
|
||||
public bool GraphicsEnableTextureRealTimeEdit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing debug log messages
|
||||
/// </summary>
|
||||
|
|
|
@ -464,10 +464,30 @@ namespace Ryujinx.UI.Common.Configuration
|
|||
public ReactiveObject<float> ResScaleCustom { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Dumps shaders in this local directory
|
||||
/// Directory to save the game shaders.
|
||||
/// </summary>
|
||||
public ReactiveObject<string> ShadersDumpPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Directory to save the game textures, if texture dumping is enabled.
|
||||
/// </summary>
|
||||
public ReactiveObject<string> TexturesDumpPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// File format used to dump textures.
|
||||
/// </summary>
|
||||
public ReactiveObject<TextureFileFormat> TexturesDumpFileFormat { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables texture dumping.
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableTextureDump { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables real-time texture editing.
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableTextureRealTimeEdit { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Vertical Sync
|
||||
/// </summary>
|
||||
|
@ -531,6 +551,13 @@ namespace Ryujinx.UI.Common.Configuration
|
|||
AspectRatio = new ReactiveObject<AspectRatio>();
|
||||
AspectRatio.Event += static (sender, e) => LogValueChange(e, nameof(AspectRatio));
|
||||
ShadersDumpPath = new ReactiveObject<string>();
|
||||
TexturesDumpPath = new ReactiveObject<string>();
|
||||
TexturesDumpFileFormat = new ReactiveObject<TextureFileFormat>();
|
||||
TexturesDumpFileFormat.Event += static (sender, e) => LogValueChange(e, nameof(TexturesDumpFileFormat));
|
||||
EnableTextureDump = new ReactiveObject<bool>();
|
||||
EnableTextureDump.Event += static (sender, e) => LogValueChange(e, nameof(EnableTextureDump));
|
||||
EnableTextureRealTimeEdit = new ReactiveObject<bool>();
|
||||
EnableTextureRealTimeEdit.Event += static (sender, e) => LogValueChange(e, nameof(EnableTextureRealTimeEdit));
|
||||
EnableVsync = new ReactiveObject<bool>();
|
||||
EnableVsync.Event += static (sender, e) => LogValueChange(e, nameof(EnableVsync));
|
||||
EnableShaderCache = new ReactiveObject<bool>();
|
||||
|
@ -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;
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -295,6 +295,48 @@
|
|||
ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale TextureDumpPathTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsTextureDumpPath}"
|
||||
Width="250" />
|
||||
<TextBox Text="{Binding TextureDumpPath}"
|
||||
Width="350"
|
||||
ToolTip.Tip="{locale:Locale TextureDumpPathTooltip}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale GraphicsTextureDumpFormatTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsTextureDumpFormat}"
|
||||
Width="250" />
|
||||
<ComboBox Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale GraphicsTextureDumpFormatTooltip}"
|
||||
SelectedIndex="{Binding TextureDumpFormatIndex}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsTextureDumpFormatDds}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsTextureDumpFormatPng}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnableTextureDump}"
|
||||
ToolTip.Tip="{locale:Locale GraphicsEnableTextureDumpTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsEnableTextureDump}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableTextureRealTimeEditing}"
|
||||
ToolTip.Tip="{locale:Locale GraphicsEnableTextureRealTimeEditingTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsEnableTextureRealTimeEditing}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue