Initial texture replacement implementation

This commit is contained in:
gdkchan 2024-08-31 15:58:34 -03:00
parent 2c5c0392f9
commit bab9477656
26 changed files with 3510 additions and 68 deletions

View 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,
}
}

View file

@ -2,6 +2,7 @@ using Ryujinx.Common;
using Ryujinx.Graphics.Device; using Ryujinx.Graphics.Device;
using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Engine.GPFifo; using Ryujinx.Graphics.Gpu.Engine.GPFifo;
using Ryujinx.Graphics.Gpu.Image;
using Ryujinx.Graphics.Gpu.Memory; using Ryujinx.Graphics.Gpu.Memory;
using Ryujinx.Graphics.Gpu.Shader; using Ryujinx.Graphics.Gpu.Shader;
using Ryujinx.Graphics.Gpu.Synchronization; using Ryujinx.Graphics.Gpu.Synchronization;
@ -45,6 +46,8 @@ namespace Ryujinx.Graphics.Gpu
/// </summary> /// </summary>
public Window Window { get; } public Window Window { get; }
public DiskTextureStorage DiskTextureStorage { get; }
/// <summary> /// <summary>
/// Internal sequence number, used to avoid needless resource data updates /// Internal sequence number, used to avoid needless resource data updates
/// in the middle of a command buffer before synchronizations. /// in the middle of a command buffer before synchronizations.
@ -123,6 +126,8 @@ namespace Ryujinx.Graphics.Gpu
Window = new Window(this); Window = new Window(this);
DiskTextureStorage = new DiskTextureStorage();
HostInitalized = new ManualResetEvent(false); HostInitalized = new ManualResetEvent(false);
_gpuReadyEvent = new ManualResetEvent(false); _gpuReadyEvent = new ManualResetEvent(false);
@ -283,6 +288,8 @@ namespace Ryujinx.Graphics.Gpu
physicalMemory.ShaderCache.Initialize(cancellationToken); physicalMemory.ShaderCache.Initialize(cancellationToken);
} }
DiskTextureStorage.Initialize();
_gpuReadyEvent.Set(); _gpuReadyEvent.Set();
} }

View file

@ -49,7 +49,7 @@ namespace Ryujinx.Graphics.Gpu
/// <summary> /// <summary>
/// Title id of the current running game. /// Title id of the current running game.
/// Used by the shader cache. /// Used by the shader cache and texture dumping.
/// </summary> /// </summary>
public static string TitleId; public static string TitleId;
@ -72,6 +72,26 @@ namespace Ryujinx.Graphics.Gpu
/// Enables or disables color space passthrough, if available. /// Enables or disables color space passthrough, if available.
/// </summary> /// </summary>
public static bool EnableColorSpacePassthrough = false; 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 #pragma warning restore CA2211
} }

File diff suppressed because it is too large Load diff

View file

@ -65,6 +65,16 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary> /// </summary>
public int Height { get; private set; } 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> /// <summary>
/// Texture information. /// Texture information.
/// </summary> /// </summary>
@ -107,11 +117,13 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary> /// </summary>
public int InvalidatedSequence { get; private set; } public int InvalidatedSequence { get; private set; }
private int _depth;
private int _layers;
public int FirstLayer { get; private set; } public int FirstLayer { get; private set; }
public int FirstLevel { get; private set; } public int FirstLevel { get; private set; }
private TextureInfoOverride? _importOverride;
private bool _forceReimport;
private readonly bool _forRender;
private bool _hasData; private bool _hasData;
private bool _dirty = true; private bool _dirty = true;
private int _updateCount; 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="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="scaleFactor">The floating point scale factor to initialize with</param>
/// <param name="scaleMode">The scale mode 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( private Texture(
GpuContext context, GpuContext context,
PhysicalMemory physicalMemory, PhysicalMemory physicalMemory,
@ -199,7 +212,8 @@ namespace Ryujinx.Graphics.Gpu.Image
int firstLayer, int firstLayer,
int firstLevel, int firstLevel,
float scaleFactor, float scaleFactor,
TextureScaleMode scaleMode) TextureScaleMode scaleMode,
bool forRender)
{ {
InitializeTexture(context, physicalMemory, info, sizeInfo, range); InitializeTexture(context, physicalMemory, info, sizeInfo, range);
@ -209,6 +223,8 @@ namespace Ryujinx.Graphics.Gpu.Image
ScaleFactor = scaleFactor; ScaleFactor = scaleFactor;
ScaleMode = scaleMode; ScaleMode = scaleMode;
_forRender = forRender;
InitializeData(true); InitializeData(true);
} }
@ -221,17 +237,21 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="sizeInfo">Size information of the texture</param> /// <param name="sizeInfo">Size information of the texture</param>
/// <param name="range">Physical memory ranges where the texture data is located</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="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( public Texture(
GpuContext context, GpuContext context,
PhysicalMemory physicalMemory, PhysicalMemory physicalMemory,
TextureInfo info, TextureInfo info,
SizeInfo sizeInfo, SizeInfo sizeInfo,
MultiRange range, MultiRange range,
TextureScaleMode scaleMode) TextureScaleMode scaleMode,
bool forRender)
{ {
ScaleFactor = 1f; // Texture is first loaded at scale 1x. ScaleFactor = 1f; // Texture is first loaded at scale 1x.
ScaleMode = scaleMode; ScaleMode = scaleMode;
_forRender = forRender;
InitializeTexture(context, physicalMemory, info, sizeInfo, range); InitializeTexture(context, physicalMemory, info, sizeInfo, range);
} }
@ -279,7 +299,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{ {
Debug.Assert(!isView); 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); HostTexture = _context.Renderer.CreateTexture(createInfo);
SynchronizeMemory(); // Load the data. SynchronizeMemory(); // Load the data.
@ -303,7 +323,7 @@ namespace Ryujinx.Graphics.Gpu.Image
ScaleFactor = GraphicsConfig.ResScale; 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); HostTexture = _context.Renderer.CreateTexture(createInfo);
} }
} }
@ -345,9 +365,10 @@ namespace Ryujinx.Graphics.Gpu.Image
FirstLayer + firstLayer, FirstLayer + firstLayer,
FirstLevel + firstLevel, FirstLevel + firstLevel,
ScaleFactor, 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); texture.HostTexture = HostTexture.CreateView(createInfo, firstLayer, firstLevel);
_viewStorage.AddView(texture); _viewStorage.AddView(texture);
@ -491,7 +512,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{ {
if (storage == null) 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); 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}."); Logger.Debug?.Print(LogClass.Gpu, $" Recreating view {Info.Width}x{Info.Height} {Info.FormatInfo.Format}.");
view.ScaleFactor = scale; 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); ITexture newView = HostTexture.CreateView(viewCreateInfo, view.FirstLayer - FirstLayer, view.FirstLevel - FirstLevel);
view.ReplaceStorage(newView); view.ReplaceStorage(newView);
@ -572,6 +593,11 @@ namespace Ryujinx.Graphics.Gpu.Image
return Group.CheckDirty(this, consume); return Group.CheckDirty(this, consume);
} }
public void ForceReimport()
{
_forceReimport = true;
}
/// <summary> /// <summary>
/// Discards all data for this texture. /// Discards all data for this texture.
/// This clears all dirty flags and pending copies from other textures. /// This clears all dirty flags and pending copies from other textures.
@ -598,6 +624,15 @@ namespace Ryujinx.Graphics.Gpu.Image
return; return;
} }
if (_forceReimport)
{
SynchronizeFull();
_forceReimport = false;
return;
}
if (!_dirty) if (!_dirty)
{ {
return; return;
@ -644,7 +679,7 @@ namespace Ryujinx.Graphics.Gpu.Image
// The decompression is slow, so we want to avoid it as much as possible. // 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 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. // 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) if (_updateCount < ByteComparisonSwitchThreshold)
{ {
@ -689,6 +724,11 @@ namespace Ryujinx.Graphics.Gpu.Image
{ {
BlacklistScale(); BlacklistScale();
if (HasImportOverride())
{
return;
}
Group.CheckDirty(this, true); Group.CheckDirty(this, true);
AlwaysFlushOnOverlap = true; AlwaysFlushOnOverlap = true;
@ -726,6 +766,11 @@ namespace Ryujinx.Graphics.Gpu.Image
{ {
BlacklistScale(); BlacklistScale();
if (HasImportOverride())
{
return;
}
HostTexture.SetData(data, layer, level, region); HostTexture.SetData(data, layer, level, region);
_currentData = null; _currentData = null;
@ -745,8 +790,8 @@ namespace Ryujinx.Graphics.Gpu.Image
int width = Info.Width; int width = Info.Width;
int height = Info.Height; int height = Info.Height;
int depth = _depth; int depth = Depth;
int layers = single ? 1 : _layers; int layers = single ? 1 : Layers;
int levels = single ? 1 : (Info.Levels - level); int levels = single ? 1 : (Info.Levels - level);
width = Math.Max(width >> level, 1); width = Math.Max(width >> level, 1);
@ -789,11 +834,55 @@ namespace Ryujinx.Graphics.Gpu.Image
} }
IMemoryOwner<byte> result = linear; 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: // Handle compressed cases not supported by the host:
// - ASTC is usually not supported on desktop cards. // - ASTC is usually not supported on desktop cards.
// - BC4/BC5 is not supported on 3D textures. // - BC4/BC5 is not supported on 3D textures.
if (!_context.Capabilities.SupportsAstcCompression && Format.IsAstc()) if (!_context.Capabilities.SupportsAstcCompression && formatInfo.Format.IsAstc())
{ {
using (result) using (result)
{ {
@ -824,9 +913,9 @@ namespace Ryujinx.Graphics.Gpu.Image
return decoded; 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.Etc2RgbaSrgb:
case Format.Etc2RgbaUnorm: 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.Bc1RgbaSrgb:
case Format.Bc1RgbaUnorm: 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) 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) 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.B5G6R5Unorm:
case Format.R5G6B5Unorm: case Format.R5G6B5Unorm:
@ -973,8 +1062,8 @@ namespace Ryujinx.Graphics.Gpu.Image
int width = Info.Width; int width = Info.Width;
int height = Info.Height; int height = Info.Height;
int depth = _depth; int depth = Depth;
int layers = single ? 1 : _layers; int layers = single ? 1 : Layers;
int levels = single ? 1 : (Info.Levels - level); int levels = single ? 1 : (Info.Levels - level);
width = Math.Max(width >> level, 1); width = Math.Max(width >> level, 1);
@ -1029,7 +1118,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <returns>True if data was flushed, false otherwise</returns> /// <returns>True if data was flushed, false otherwise</returns>
public bool FlushModified(bool tracked = true) 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> /// <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> /// <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) public void Flush(bool tracked)
{ {
if (TextureCompatibility.CanTextureFlush(Info, _context.Capabilities)) if (TextureCompatibility.CanTextureFlush(this, _context.Capabilities))
{ {
FlushTextureDataToGuest(tracked); FlushTextureDataToGuest(tracked);
} }
@ -1299,6 +1388,22 @@ namespace Ryujinx.Graphics.Gpu.Image
return result; 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> /// <summary>
/// Gets a texture of the specified target type from this texture. /// 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. /// 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)) 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( TextureCreateInfo createInfo = new(
Info.Width, Info.Width,
@ -1418,7 +1523,7 @@ namespace Ryujinx.Graphics.Gpu.Image
foreach (Texture view in viewCopy) 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); ITexture newView = parent.HostTexture.CreateView(createInfo, view.FirstLayer + firstLayer, view.FirstLevel + firstLevel);
@ -1453,8 +1558,8 @@ namespace Ryujinx.Graphics.Gpu.Image
Height = info.Height; Height = info.Height;
CanForceAnisotropy = CanTextureForceAnisotropy(); CanForceAnisotropy = CanTextureForceAnisotropy();
_depth = info.GetDepth(); Depth = info.GetDepth();
_layers = info.GetLayers(); Layers = info.GetLayers();
} }
/// <summary> /// <summary>

View file

@ -772,6 +772,11 @@ namespace Ryujinx.Graphics.Gpu.Image
out int firstLevel, out int firstLevel,
flags); flags);
if (overlap.HasImportOverride())
{
overlapCompatibility = TextureViewCompatibility.Incompatible;
}
if (overlapCompatibility >= TextureViewCompatibility.FormatAlias) if (overlapCompatibility >= TextureViewCompatibility.FormatAlias)
{ {
if (overlap.IsView) 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. // 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, // 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. // 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. // No match, create a new texture.
if (texture == null) 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. // 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. // 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 firstLayer,
out int firstLevel); out int firstLevel);
if (overlap.HasImportOverride())
{
compatibility = TextureViewCompatibility.Incompatible;
}
if (overlap.IsView && compatibility == TextureViewCompatibility.Full) if (overlap.IsView && compatibility == TextureViewCompatibility.Full)
{ {
compatibility = TextureViewCompatibility.CopyOnly; compatibility = TextureViewCompatibility.CopyOnly;
@ -1023,6 +1033,12 @@ namespace Ryujinx.Graphics.Gpu.Image
continue; continue;
} }
if (texture.HasImportOverride())
{
// Replaced textures with different parameters are not considered compatible.
continue;
}
// Note: If we allow different sizes for those overlaps, // 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. // 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. // Since this is not allowed right now, we don't need to do it.
@ -1046,14 +1062,11 @@ namespace Ryujinx.Graphics.Gpu.Image
} }
else 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); ITexture newView = texture.HostTexture.CreateView(createInfo, oInfo.FirstLayer, oInfo.FirstLevel);
overlap.SynchronizeMemory(); overlap.SynchronizeMemory();
overlap.HostTexture.CopyTo(newView, 0, 0); overlap.HostTexture.CopyTo(newView, 0, 0);
overlap.ReplaceView(texture, overlapInfo, newView, oInfo.FirstLayer, oInfo.FirstLevel); 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="info">Texture information</param>
/// <param name="caps">GPU capabilities</param> /// <param name="caps">GPU capabilities</param>
/// <param name="scale">Texture scale factor, to be applied to the texture size</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> /// <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) 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) if (scale != 1f)
{ {
width = (int)MathF.Ceiling(width * scale); width = (int)MathF.Ceiling(width * scale);
@ -1269,7 +1297,7 @@ namespace Ryujinx.Graphics.Gpu.Image
width, width,
height, height,
depth, depth,
info.Levels, levels,
info.Samples, info.Samples,
formatInfo.BlockWidth, formatInfo.BlockWidth,
formatInfo.BlockHeight, formatInfo.BlockHeight,

View file

@ -50,10 +50,10 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="info">Texture information</param> /// <param name="info">Texture information</param>
/// <param name="caps">Host GPU capabilities</param> /// <param name="caps">Host GPU capabilities</param>
/// <returns>True if the format is incompatible, false otherwise</returns> /// <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; Format originalFormat = info.FormatInfo.Format;
return ToHostCompatibleFormat(info, caps).Format != originalFormat; return ToHostCompatibleFormat(info.FormatInfo, info.Target, caps).Format != originalFormat;
} }
/// <summary> /// <summary>
@ -64,10 +64,11 @@ namespace Ryujinx.Graphics.Gpu.Image
/// This can be used to convert a incompatible compressed format to the decompressor /// This can be used to convert a incompatible compressed format to the decompressor
/// output format. /// output format.
/// </remarks> /// </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> /// <param name="caps">Host GPU capabilities</param>
/// <returns>A host compatible format</returns> /// <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. // The host API does not support those compressed formats.
// We assume software decompression will be done for those textures, // We assume software decompression will be done for those textures,
@ -75,13 +76,13 @@ namespace Ryujinx.Graphics.Gpu.Image
if (!caps.SupportsAstcCompression) if (!caps.SupportsAstcCompression)
{ {
if (info.FormatInfo.Format.IsAstcUnorm()) if (formatInfo.Format.IsAstcUnorm())
{ {
return GraphicsConfig.EnableTextureRecompression return GraphicsConfig.EnableTextureRecompression
? new FormatInfo(Format.Bc7Unorm, 4, 4, 16, 4) ? new FormatInfo(Format.Bc7Unorm, 4, 4, 16, 4)
: new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4); : new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4);
} }
else if (info.FormatInfo.Format.IsAstcSrgb()) else if (formatInfo.Format.IsAstcSrgb())
{ {
return GraphicsConfig.EnableTextureRecompression return GraphicsConfig.EnableTextureRecompression
? new FormatInfo(Format.Bc7Srgb, 4, 4, 16, 4) ? 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.Bc1RgbaSrgb:
case Format.Bc2Srgb: case Format.Bc2Srgb:
@ -119,7 +120,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (!caps.SupportsEtc2Compression) if (!caps.SupportsEtc2Compression)
{ {
switch (info.FormatInfo.Format) switch (formatInfo.Format)
{ {
case Format.Etc2RgbaSrgb: case Format.Etc2RgbaSrgb:
case Format.Etc2RgbPtaSrgb: 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) 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) if (!caps.SupportsR4G4B4A4Format)
{ {
return new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4); 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> /// <summary>
@ -166,7 +167,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="target">Target usage of the texture</param> /// <param name="target">Target usage of the texture</param>
/// <param name="caps">Host GPU Capabilities</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> /// <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; bool not3DOr3DCompressionSupported = target != Target.Texture3D || caps.Supports3DTextureCompression;
@ -194,15 +195,26 @@ namespace Ryujinx.Graphics.Gpu.Image
return true; 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> /// <summary>
/// Determines whether a texture can flush its data back to guest memory. /// Determines whether a texture can flush its data back to guest memory.
/// </summary> /// </summary>
/// <param name="info">Texture information</param> /// <param name="info">Texture information</param>
/// <param name="caps">Host GPU Capabilities</param> /// <param name="caps">Host GPU Capabilities</param>
/// <returns>True if the texture can flush, false otherwise</returns> /// <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. return false; // Flushing this format is not supported, as it may have been converted to another host format.
} }

View file

@ -629,7 +629,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (_flushBuffer == BufferHandle.Null) if (_flushBuffer == BufferHandle.Null)
{ {
if (!TextureCompatibility.CanTextureFlush(Storage.Info, _context.Capabilities)) if (!TextureCompatibility.CanTextureFlush(Storage, _context.Capabilities))
{ {
return; 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()); FlushSliceRange(false, handle.BaseSlice, handle.BaseSlice + handle.SliceCount, inBuffer, Storage.GetFlushTexture());
} }

View file

@ -207,6 +207,16 @@ namespace Ryujinx.Graphics.Gpu.Image
return GetLayers(Target, DepthOrLayers); 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> /// <summary>
/// Gets the number of layers of the texture. /// 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. /// 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> /// <summary>
/// Gets the number of 2D slices of the texture. /// 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. /// Returns 6 for cubemap textures, layer faces for cubemap array textures, and DepthOrLayers for everything else.

View 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);
}
}
}

View 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
}
}

View 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,
};
}
}
}

View 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,
}
}

View file

@ -0,0 +1,11 @@
namespace Ryujinx.Graphics.Texture.FileFormats
{
public enum ImageDimensions
{
Dim2D,
Dim2DArray,
Dim3D,
DimCube,
DimCubeArray,
}
}

View 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,
}
}

View file

@ -0,0 +1,12 @@
namespace Ryujinx.Graphics.Texture.FileFormats
{
public enum ImageLoadResult
{
Success,
CorruptedHeader,
CorruptedData,
DataTooShort,
OutputTooShort,
UnsupportedFormat,
}
}

View 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;
}
}
}

View 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;
}
}
}

View file

@ -8,6 +8,7 @@ using LibHac.Tools.FsSystem.RomFs;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.HLE.Loaders.Mods; using Ryujinx.HLE.Loaders.Mods;
@ -28,6 +29,7 @@ namespace Ryujinx.HLE.HOS
private const string RomfsDir = "romfs"; private const string RomfsDir = "romfs";
private const string ExefsDir = "exefs"; private const string ExefsDir = "exefs";
private const string CheatDir = "cheats"; private const string CheatDir = "cheats";
private const string TexturesDir = "textures";
private const string RomfsContainer = "romfs.bin"; private const string RomfsContainer = "romfs.bin";
private const string ExefsContainer = "exefs.nsp"; private const string ExefsContainer = "exefs.nsp";
private const string StubExtension = ".stub"; private const string StubExtension = ".stub";
@ -81,6 +83,7 @@ namespace Ryujinx.HLE.HOS
public List<Mod<DirectoryInfo>> RomfsDirs { get; } public List<Mod<DirectoryInfo>> RomfsDirs { get; }
public List<Mod<DirectoryInfo>> ExefsDirs { get; } public List<Mod<DirectoryInfo>> ExefsDirs { get; }
public List<Mod<DirectoryInfo>> TextureDirs { get; }
public List<Cheat> Cheats { get; } public List<Cheat> Cheats { get; }
@ -90,6 +93,7 @@ namespace Ryujinx.HLE.HOS
ExefsContainers = new List<Mod<FileInfo>>(); ExefsContainers = new List<Mod<FileInfo>>();
RomfsDirs = new List<Mod<DirectoryInfo>>(); RomfsDirs = new List<Mod<DirectoryInfo>>();
ExefsDirs = new List<Mod<DirectoryInfo>>(); ExefsDirs = new List<Mod<DirectoryInfo>>();
TextureDirs = new List<Mod<DirectoryInfo>>();
Cheats = new List<Cheat>(); Cheats = new List<Cheat>();
} }
} }
@ -187,6 +191,14 @@ namespace Ryujinx.HLE.HOS
mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>(dir.Name, modDir, enabled)); mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>(dir.Name, modDir, enabled));
types.Append('E'); 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)) else if (StrEquals(CheatDir, modDir.Name))
{ {
types.Append('C', QueryCheatsDir(mods, modDir)); types.Append('C', QueryCheatsDir(mods, modDir));
@ -699,6 +711,23 @@ namespace Ryujinx.HLE.HOS
return ApplyProgramPatches(nsoMods, 0x100, programs); 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) internal void LoadCheats(ulong applicationId, ProcessTamperInfo tamperInfo, TamperMachine tamperMachine)
{ {
if (tamperInfo?.BuildIds == null || tamperInfo.CodeAddresses == null) if (tamperInfo?.BuildIds == null || tamperInfo.CodeAddresses == null)

View file

@ -84,7 +84,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
// Don't use PTC if ExeFS files have been replaced. // Don't use PTC if ExeFS files have been replaced.
bool enablePtc = device.System.EnablePtc && !modLoadResult.Modified; bool enablePtc = device.System.EnablePtc && !modLoadResult.Modified;
if (!enablePtc) if (modLoadResult.Modified)
{ {
Logger.Warning?.Print(LogClass.Ptc, "Detected unsupported ExeFs modifications. PTC disabled."); 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}"; Graphics.Gpu.GraphicsConfig.TitleId = $"{programId:x16}";
device.Gpu.HostInitalized.Set(); device.Gpu.HostInitalized.Set();
// Load texture replacements.
device.Configuration.VirtualFileSystem.ModLoader.ApplyTextureMods(programId, device.Gpu);
if (!MemoryBlock.SupportsFlags(MemoryAllocationFlags.ViewCompatible)) if (!MemoryBlock.SupportsFlags(MemoryAllocationFlags.ViewCompatible))
{ {
device.Configuration.MemoryManagerMode = MemoryManagerMode.SoftwarePageTable; device.Configuration.MemoryManagerMode = MemoryManagerMode.SoftwarePageTable;

View file

@ -15,7 +15,7 @@ namespace Ryujinx.UI.Common.Configuration
/// <summary> /// <summary>
/// The current version of the file format /// The current version of the file format
/// </summary> /// </summary>
public const int CurrentVersion = 51; public const int CurrentVersion = 52;
/// <summary> /// <summary>
/// Version of the configuration file format /// Version of the configuration file format
@ -68,10 +68,30 @@ namespace Ryujinx.UI.Common.Configuration
public int ScalingFilterLevel { get; set; } public int ScalingFilterLevel { get; set; }
/// <summary> /// <summary>
/// Dumps shaders in this local directory /// Directory to save the game shaders.
/// </summary> /// </summary>
public string GraphicsShadersDumpPath { get; set; } 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> /// <summary>
/// Enables printing debug log messages /// Enables printing debug log messages
/// </summary> /// </summary>

View file

@ -464,10 +464,30 @@ namespace Ryujinx.UI.Common.Configuration
public ReactiveObject<float> ResScaleCustom { get; private set; } public ReactiveObject<float> ResScaleCustom { get; private set; }
/// <summary> /// <summary>
/// Dumps shaders in this local directory /// Directory to save the game shaders.
/// </summary> /// </summary>
public ReactiveObject<string> ShadersDumpPath { get; private set; } 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> /// <summary>
/// Enables or disables Vertical Sync /// Enables or disables Vertical Sync
/// </summary> /// </summary>
@ -531,6 +551,13 @@ namespace Ryujinx.UI.Common.Configuration
AspectRatio = new ReactiveObject<AspectRatio>(); AspectRatio = new ReactiveObject<AspectRatio>();
AspectRatio.Event += static (sender, e) => LogValueChange(e, nameof(AspectRatio)); AspectRatio.Event += static (sender, e) => LogValueChange(e, nameof(AspectRatio));
ShadersDumpPath = new ReactiveObject<string>(); 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 = new ReactiveObject<bool>();
EnableVsync.Event += static (sender, e) => LogValueChange(e, nameof(EnableVsync)); EnableVsync.Event += static (sender, e) => LogValueChange(e, nameof(EnableVsync));
EnableShaderCache = new ReactiveObject<bool>(); EnableShaderCache = new ReactiveObject<bool>();
@ -673,6 +700,10 @@ namespace Ryujinx.UI.Common.Configuration
ScalingFilter = Graphics.ScalingFilter, ScalingFilter = Graphics.ScalingFilter,
ScalingFilterLevel = Graphics.ScalingFilterLevel, ScalingFilterLevel = Graphics.ScalingFilterLevel,
GraphicsShadersDumpPath = Graphics.ShadersDumpPath, GraphicsShadersDumpPath = Graphics.ShadersDumpPath,
GraphicsTexturesDumpPath = Graphics.TexturesDumpPath,
GraphicsTexturesDumpFileFormat = Graphics.TexturesDumpFileFormat,
GraphicsEnableTextureDump = Graphics.EnableTextureDump,
GraphicsEnableTextureRealTimeEdit = Graphics.EnableTextureRealTimeEdit,
LoggingEnableDebug = Logger.EnableDebug, LoggingEnableDebug = Logger.EnableDebug,
LoggingEnableStub = Logger.EnableStub, LoggingEnableStub = Logger.EnableStub,
LoggingEnableInfo = Logger.EnableInfo, LoggingEnableInfo = Logger.EnableInfo,
@ -782,6 +813,10 @@ namespace Ryujinx.UI.Common.Configuration
Graphics.GraphicsBackend.Value = DefaultGraphicsBackend(); Graphics.GraphicsBackend.Value = DefaultGraphicsBackend();
Graphics.PreferredGpu.Value = ""; Graphics.PreferredGpu.Value = "";
Graphics.ShadersDumpPath.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.EnableDebug.Value = false;
Logger.EnableStub.Value = true; Logger.EnableStub.Value = true;
Logger.EnableInfo.Value = true; Logger.EnableInfo.Value = true;
@ -1477,12 +1512,28 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileUpdated = true; 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; Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog;
Graphics.ResScale.Value = configurationFileFormat.ResScale; Graphics.ResScale.Value = configurationFileFormat.ResScale;
Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;
Graphics.MaxAnisotropy.Value = configurationFileFormat.MaxAnisotropy; Graphics.MaxAnisotropy.Value = configurationFileFormat.MaxAnisotropy;
Graphics.AspectRatio.Value = configurationFileFormat.AspectRatio; Graphics.AspectRatio.Value = configurationFileFormat.AspectRatio;
Graphics.ShadersDumpPath.Value = configurationFileFormat.GraphicsShadersDumpPath; 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.BackendThreading.Value = configurationFileFormat.BackendThreading;
Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend; Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend;
Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu; Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu;

View file

@ -171,6 +171,12 @@
"SettingsTabGraphicsAspectRatioStretch": "Stretch to Fit Window", "SettingsTabGraphicsAspectRatioStretch": "Stretch to Fit Window",
"SettingsTabGraphicsDeveloperOptions": "Developer Options", "SettingsTabGraphicsDeveloperOptions": "Developer Options",
"SettingsTabGraphicsShaderDumpPath": "Graphics Shader Dump Path:", "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", "SettingsTabLogging": "Logging",
"SettingsTabLoggingLogging": "Logging", "SettingsTabLoggingLogging": "Logging",
"SettingsTabLoggingEnableLoggingToFile": "Enable Logging to File", "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.", "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.", "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", "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.", "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.", "StubLogTooltip": "Prints stub log messages in the console. Does not affect performance.",
"InfoLogTooltip": "Prints info log messages in the console. Does not affect performance.", "InfoLogTooltip": "Prints info log messages in the console. Does not affect performance.",

View file

@ -168,6 +168,11 @@ namespace Ryujinx.Ava.UI.ViewModels
public string TimeZone { get; set; } public string TimeZone { get; set; }
public string ShaderDumpPath { 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 Language { get; set; }
public int Region { get; set; } public int Region { get; set; }
public int FsGlobalAccessLogMode { get; set; } public int FsGlobalAccessLogMode { get; set; }
@ -447,6 +452,10 @@ namespace Ryujinx.Ava.UI.ViewModels
AspectRatio = (int)config.Graphics.AspectRatio.Value; AspectRatio = (int)config.Graphics.AspectRatio.Value;
GraphicsBackendMultithreadingIndex = (int)config.Graphics.BackendThreading.Value; GraphicsBackendMultithreadingIndex = (int)config.Graphics.BackendThreading.Value;
ShaderDumpPath = config.Graphics.ShadersDumpPath; 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; AntiAliasingEffect = (int)config.Graphics.AntiAliasing.Value;
ScalingFilter = (int)config.Graphics.ScalingFilter.Value; ScalingFilter = (int)config.Graphics.ScalingFilter.Value;
ScalingFilterLevel = config.Graphics.ScalingFilterLevel.Value; ScalingFilterLevel = config.Graphics.ScalingFilterLevel.Value;
@ -550,6 +559,10 @@ namespace Ryujinx.Ava.UI.ViewModels
config.Graphics.BackendThreading.Value = (BackendThreading)GraphicsBackendMultithreadingIndex; config.Graphics.BackendThreading.Value = (BackendThreading)GraphicsBackendMultithreadingIndex;
config.Graphics.ShadersDumpPath.Value = ShaderDumpPath; 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 // Audio
AudioBackend audioBackend = (AudioBackend)AudioBackend; AudioBackend audioBackend = (AudioBackend)AudioBackend;

View file

@ -295,6 +295,48 @@
ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}" /> ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}" />
</StackPanel> </StackPanel>
</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> </StackPanel>
</Border> </Border>
</ScrollViewer> </ScrollViewer>

View file

@ -12,6 +12,7 @@ using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Applet; using Ryujinx.Ava.UI.Applet;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Gpu; using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
@ -512,6 +513,10 @@ namespace Ryujinx.Ava.UI.Windows
GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache; GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache;
GraphicsConfig.EnableTextureRecompression = ConfigurationState.Instance.Graphics.EnableTextureRecompression; GraphicsConfig.EnableTextureRecompression = ConfigurationState.Instance.Graphics.EnableTextureRecompression;
GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE; 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 #pragma warning restore IDE0055
} }