diff --git a/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs b/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs
index d67d71b83..13dad62ce 100644
--- a/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs
+++ b/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs
@@ -13,10 +13,20 @@ namespace Ryujinx.Configuration
///
/// The current version of the file format
///
- public const int CurrentVersion = 10;
+ public const int CurrentVersion = 11;
public int Version { get; set; }
+ ///
+ /// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead.
+ ///
+ public int ResScale { get; set; }
+
+ ///
+ /// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1.
+ ///
+ public float ResScaleCustom { get; set; }
+
///
/// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide.
///
diff --git a/Ryujinx.Common/Configuration/ConfigurationState.cs b/Ryujinx.Common/Configuration/ConfigurationState.cs
index 0f5367f9f..3149f250f 100644
--- a/Ryujinx.Common/Configuration/ConfigurationState.cs
+++ b/Ryujinx.Common/Configuration/ConfigurationState.cs
@@ -271,6 +271,16 @@ namespace Ryujinx.Configuration
///
public ReactiveObject MaxAnisotropy { get; private set; }
+ ///
+ /// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead.
+ ///
+ public ReactiveObject ResScale { get; private set; }
+
+ ///
+ /// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1.
+ ///
+ public ReactiveObject ResScaleCustom { get; private set; }
+
///
/// Dumps shaders in this local directory
///
@@ -283,6 +293,8 @@ namespace Ryujinx.Configuration
public GraphicsSection()
{
+ ResScale = new ReactiveObject();
+ ResScaleCustom = new ReactiveObject();
MaxAnisotropy = new ReactiveObject();
ShadersDumpPath = new ReactiveObject();
EnableVsync = new ReactiveObject();
@@ -354,6 +366,8 @@ namespace Ryujinx.Configuration
ConfigurationFileFormat configurationFile = new ConfigurationFileFormat
{
Version = ConfigurationFileFormat.CurrentVersion,
+ ResScale = Graphics.ResScale,
+ ResScaleCustom = Graphics.ResScaleCustom,
MaxAnisotropy = Graphics.MaxAnisotropy,
GraphicsShadersDumpPath = Graphics.ShadersDumpPath,
LoggingEnableDebug = Logger.EnableDebug,
@@ -410,6 +424,8 @@ namespace Ryujinx.Configuration
public void LoadDefault()
{
+ Graphics.ResScale.Value = 1;
+ Graphics.ResScaleCustom.Value = 1.0f;
Graphics.MaxAnisotropy.Value = -1;
Graphics.ShadersDumpPath.Value = "";
Logger.EnableDebug.Value = false;
@@ -652,10 +668,22 @@ namespace Ryujinx.Configuration
configurationFileUpdated = true;
}
+ if (configurationFileFormat.Version < 11)
+ {
+ Common.Logging.Logger.PrintWarning(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 11.");
+
+ configurationFileFormat.ResScale = 1;
+ configurationFileFormat.ResScaleCustom = 1.0f;
+
+ configurationFileUpdated = true;
+ }
+
List inputConfig = new List();
inputConfig.AddRange(configurationFileFormat.ControllerConfig);
inputConfig.AddRange(configurationFileFormat.KeyboardConfig);
+ Graphics.ResScale.Value = configurationFileFormat.ResScale;
+ Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;
Graphics.MaxAnisotropy.Value = configurationFileFormat.MaxAnisotropy;
Graphics.ShadersDumpPath.Value = configurationFileFormat.GraphicsShadersDumpPath;
Logger.EnableDebug.Value = configurationFileFormat.LoggingEnableDebug;
diff --git a/Ryujinx.Graphics.GAL/Format.cs b/Ryujinx.Graphics.GAL/Format.cs
index 0221746b3..19939c38a 100644
--- a/Ryujinx.Graphics.GAL/Format.cs
+++ b/Ryujinx.Graphics.GAL/Format.cs
@@ -162,11 +162,21 @@ namespace Ryujinx.Graphics.GAL
public static class FormatExtensions
{
+ ///
+ /// Checks if the texture format is an ASTC format.
+ ///
+ /// Texture format
+ /// True if the texture format is an ASTC format, false otherwise
public static bool IsAstc(this Format format)
{
return format.IsAstcUnorm() || format.IsAstcSrgb();
}
+ ///
+ /// Checks if the texture format is an ASTC Unorm format.
+ ///
+ /// Texture format
+ /// True if the texture format is an ASTC Unorm format, false otherwise
public static bool IsAstcUnorm(this Format format)
{
switch (format)
@@ -191,6 +201,11 @@ namespace Ryujinx.Graphics.GAL
return false;
}
+ ///
+ /// Checks if the texture format is an ASTC SRGB format.
+ ///
+ /// Texture format
+ /// True if the texture format is an ASTC SRGB format, false otherwise
public static bool IsAstcSrgb(this Format format)
{
switch (format)
@@ -214,5 +229,131 @@ namespace Ryujinx.Graphics.GAL
return false;
}
+
+ ///
+ /// Checks if the texture format is a depth, stencil or depth-stencil format.
+ ///
+ /// Texture format
+ /// True if the format is a depth, stencil or depth-stencil format, false otherwise
+ public static bool IsDepthOrStencil(this Format format)
+ {
+ switch (format)
+ {
+ case Format.D16Unorm:
+ case Format.D24UnormS8Uint:
+ case Format.D24X8Unorm:
+ case Format.D32Float:
+ case Format.D32FloatS8Uint:
+ case Format.S8Uint:
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Checks if the texture format is an unsigned integer color format.
+ ///
+ /// Texture format
+ /// True if the texture format is an unsigned integer color format, false otherwise
+ public static bool IsUint(this Format format)
+ {
+ switch (format)
+ {
+ case Format.R8Uint:
+ case Format.R16Uint:
+ case Format.R32Uint:
+ case Format.R8G8Uint:
+ case Format.R16G16Uint:
+ case Format.R32G32Uint:
+ case Format.R8G8B8Uint:
+ case Format.R16G16B16Uint:
+ case Format.R32G32B32Uint:
+ case Format.R8G8B8A8Uint:
+ case Format.R16G16B16A16Uint:
+ case Format.R32G32B32A32Uint:
+ case Format.R10G10B10A2Uint:
+ case Format.R8G8B8X8Uint:
+ case Format.R16G16B16X16Uint:
+ case Format.R32G32B32X32Uint:
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Checks if the texture format is a signed integer color format.
+ ///
+ /// Texture format
+ /// True if the texture format is a signed integer color format, false otherwise
+ public static bool IsSint(this Format format)
+ {
+ switch (format)
+ {
+ case Format.R8Sint:
+ case Format.R16Sint:
+ case Format.R32Sint:
+ case Format.R8G8Sint:
+ case Format.R16G16Sint:
+ case Format.R32G32Sint:
+ case Format.R8G8B8Sint:
+ case Format.R16G16B16Sint:
+ case Format.R32G32B32Sint:
+ case Format.R8G8B8A8Sint:
+ case Format.R16G16B16A16Sint:
+ case Format.R32G32B32A32Sint:
+ case Format.R10G10B10A2Sint:
+ case Format.R8G8B8X8Sint:
+ case Format.R16G16B16X16Sint:
+ case Format.R32G32B32X32Sint:
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Checks if the texture format is an integer color format.
+ ///
+ /// Texture format
+ /// True if the texture format is an integer color format, false otherwise
+ public static bool IsInteger(this Format format)
+ {
+ return format.IsUint() || format.IsSint();
+ }
+
+ ///
+ /// Checks if the texture format only has one component.
+ ///
+ /// Texture format
+ /// True if the texture format only has one component, false otherwise
+ public static bool HasOneComponent(this Format format)
+ {
+ switch (format)
+ {
+ case Format.R8Unorm:
+ case Format.R8Snorm:
+ case Format.R8Uint:
+ case Format.R8Sint:
+ case Format.R16Float:
+ case Format.R16Unorm:
+ case Format.R16Snorm:
+ case Format.R16Uint:
+ case Format.R16Sint:
+ case Format.R32Float:
+ case Format.R32Uint:
+ case Format.R32Sint:
+ case Format.R8Uscaled:
+ case Format.R8Sscaled:
+ case Format.R16Uscaled:
+ case Format.R16Sscaled:
+ case Format.R32Uscaled:
+ case Format.R32Sscaled:
+ return true;
+ }
+
+ return false;
+ }
}
}
\ No newline at end of file
diff --git a/Ryujinx.Graphics.GAL/IPipeline.cs b/Ryujinx.Graphics.GAL/IPipeline.cs
index aa59713d9..e365529bf 100644
--- a/Ryujinx.Graphics.GAL/IPipeline.cs
+++ b/Ryujinx.Graphics.GAL/IPipeline.cs
@@ -54,6 +54,8 @@ namespace Ryujinx.Graphics.GAL
void SetRasterizerDiscard(bool discard);
+ void SetRenderTargetScale(float scale);
+
void SetRenderTargetColorMasks(ReadOnlySpan componentMask);
void SetRenderTargets(ITexture[] colors, ITexture depthStencil);
@@ -84,5 +86,7 @@ namespace Ryujinx.Graphics.GAL
bool TryHostConditionalRendering(ICounterEvent value, ulong compare, bool isEqual);
bool TryHostConditionalRendering(ICounterEvent value, ICounterEvent compare, bool isEqual);
void EndHostConditionalRendering();
+
+ void UpdateRenderScale(ShaderStage stage, int textureCount);
}
}
diff --git a/Ryujinx.Graphics.GAL/IRenderer.cs b/Ryujinx.Graphics.GAL/IRenderer.cs
index c41b19fe5..1052f1476 100644
--- a/Ryujinx.Graphics.GAL/IRenderer.cs
+++ b/Ryujinx.Graphics.GAL/IRenderer.cs
@@ -16,7 +16,7 @@ namespace Ryujinx.Graphics.GAL
IProgram CreateProgram(IShader[] shaders);
ISampler CreateSampler(SamplerCreateInfo info);
- ITexture CreateTexture(TextureCreateInfo info);
+ ITexture CreateTexture(TextureCreateInfo info, float scale);
void DeleteBuffer(BufferHandle buffer);
diff --git a/Ryujinx.Graphics.GAL/ITexture.cs b/Ryujinx.Graphics.GAL/ITexture.cs
index a818f73aa..1c5b6ba5f 100644
--- a/Ryujinx.Graphics.GAL/ITexture.cs
+++ b/Ryujinx.Graphics.GAL/ITexture.cs
@@ -4,6 +4,10 @@ namespace Ryujinx.Graphics.GAL
{
public interface ITexture : IDisposable
{
+ int Width { get; }
+ int Height { get; }
+ float ScaleFactor { get; }
+
void CopyTo(ITexture destination, int firstLayer, int firstLevel);
void CopyTo(ITexture destination, Extents2D srcRegion, Extents2D dstRegion, bool linearFilter);
diff --git a/Ryujinx.Graphics.Gpu/Engine/Compute.cs b/Ryujinx.Graphics.Gpu/Engine/Compute.cs
index 4d18f4d3f..e40984af1 100644
--- a/Ryujinx.Graphics.Gpu/Engine/Compute.cs
+++ b/Ryujinx.Graphics.Gpu/Engine/Compute.cs
@@ -132,11 +132,11 @@ namespace Ryujinx.Graphics.Gpu.Engine
if (descriptor.IsBindless)
{
- textureBindings[index] = new TextureBindingInfo(target, descriptor.CbufOffset, descriptor.CbufSlot);
+ textureBindings[index] = new TextureBindingInfo(target, descriptor.CbufOffset, descriptor.CbufSlot, descriptor.Flags);
}
else
{
- textureBindings[index] = new TextureBindingInfo(target, descriptor.HandleIndex);
+ textureBindings[index] = new TextureBindingInfo(target, descriptor.HandleIndex, descriptor.Flags);
}
}
@@ -150,7 +150,7 @@ namespace Ryujinx.Graphics.Gpu.Engine
Target target = GetTarget(descriptor.Type);
- imageBindings[index] = new TextureBindingInfo(target, descriptor.HandleIndex);
+ imageBindings[index] = new TextureBindingInfo(target, descriptor.HandleIndex, descriptor.Flags);
}
TextureManager.SetComputeImages(imageBindings);
diff --git a/Ryujinx.Graphics.Gpu/Engine/MethodClear.cs b/Ryujinx.Graphics.Gpu/Engine/MethodClear.cs
index a9552762f..5da87e6ca 100644
--- a/Ryujinx.Graphics.Gpu/Engine/MethodClear.cs
+++ b/Ryujinx.Graphics.Gpu/Engine/MethodClear.cs
@@ -26,7 +26,9 @@ namespace Ryujinx.Graphics.Gpu.Engine
UpdateScissorState(state);
}
- UpdateRenderTargetState(state, useControl: false);
+ int index = (argument >> 6) & 0xf;
+
+ UpdateRenderTargetState(state, useControl: false, singleUse: index);
TextureManager.CommitGraphicsBindings();
@@ -35,8 +37,6 @@ namespace Ryujinx.Graphics.Gpu.Engine
uint componentMask = (uint)((argument >> 2) & 0xf);
- int index = (argument >> 6) & 0xf;
-
if (componentMask != 0)
{
var clearColor = state.Get(MethodOffset.ClearColors);
diff --git a/Ryujinx.Graphics.Gpu/Engine/MethodCopyTexture.cs b/Ryujinx.Graphics.Gpu/Engine/MethodCopyTexture.cs
index 4900db1b0..07afb253e 100644
--- a/Ryujinx.Graphics.Gpu/Engine/MethodCopyTexture.cs
+++ b/Ryujinx.Graphics.Gpu/Engine/MethodCopyTexture.cs
@@ -1,5 +1,6 @@
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.State;
+using System;
namespace Ryujinx.Graphics.Gpu.Engine
{
@@ -32,13 +33,18 @@ namespace Ryujinx.Graphics.Gpu.Engine
dstCopyTexture.Format = RtFormat.D32Float;
}
- Texture dstTexture = TextureManager.FindOrCreateTexture(dstCopyTexture);
+ Texture dstTexture = TextureManager.FindOrCreateTexture(dstCopyTexture, srcTexture.ScaleMode == Image.TextureScaleMode.Scaled);
if (dstTexture == null)
{
return;
}
+ if (srcTexture.ScaleFactor != dstTexture.ScaleFactor)
+ {
+ srcTexture.PropagateScale(dstTexture);
+ }
+
var control = state.Get(MethodOffset.CopyTextureControl);
var region = state.Get(MethodOffset.CopyRegion);
@@ -55,17 +61,19 @@ namespace Ryujinx.Graphics.Gpu.Engine
int dstX2 = region.DstX + region.DstWidth;
int dstY2 = region.DstY + region.DstHeight;
+ float scale = srcTexture.ScaleFactor; // src and dest scales are identical now.
+
Extents2D srcRegion = new Extents2D(
- srcX1 / srcTexture.Info.SamplesInX,
- srcY1 / srcTexture.Info.SamplesInY,
- srcX2 / srcTexture.Info.SamplesInX,
- srcY2 / srcTexture.Info.SamplesInY);
+ (int)Math.Ceiling(scale * (srcX1 / srcTexture.Info.SamplesInX)),
+ (int)Math.Ceiling(scale * (srcY1 / srcTexture.Info.SamplesInY)),
+ (int)Math.Ceiling(scale * (srcX2 / srcTexture.Info.SamplesInX)),
+ (int)Math.Ceiling(scale * (srcY2 / srcTexture.Info.SamplesInY)));
Extents2D dstRegion = new Extents2D(
- dstX1 / dstTexture.Info.SamplesInX,
- dstY1 / dstTexture.Info.SamplesInY,
- dstX2 / dstTexture.Info.SamplesInX,
- dstY2 / dstTexture.Info.SamplesInY);
+ (int)Math.Ceiling(scale * (dstX1 / dstTexture.Info.SamplesInX)),
+ (int)Math.Ceiling(scale * (dstY1 / dstTexture.Info.SamplesInY)),
+ (int)Math.Ceiling(scale * (dstX2 / dstTexture.Info.SamplesInX)),
+ (int)Math.Ceiling(scale * (dstY2 / dstTexture.Info.SamplesInY)));
bool linearFilter = control.UnpackLinearFilter();
@@ -79,17 +87,21 @@ namespace Ryujinx.Graphics.Gpu.Engine
// the second handles the region outside of the bounds).
// We must also extend the source texture by one line to ensure we can wrap on the last line.
// This is required by the (guest) OpenGL driver.
- if (srcRegion.X2 > srcTexture.Info.Width)
+ if (srcX2 / srcTexture.Info.SamplesInX > srcTexture.Info.Width)
{
srcCopyTexture.Height++;
- srcTexture = TextureManager.FindOrCreateTexture(srcCopyTexture);
+ srcTexture = TextureManager.FindOrCreateTexture(srcCopyTexture, srcTexture.ScaleMode == Image.TextureScaleMode.Scaled);
+ if (srcTexture.ScaleFactor != dstTexture.ScaleFactor)
+ {
+ srcTexture.PropagateScale(dstTexture);
+ }
srcRegion = new Extents2D(
- srcRegion.X1 - srcTexture.Info.Width,
- srcRegion.Y1 + 1,
- srcRegion.X2 - srcTexture.Info.Width,
- srcRegion.Y2 + 1);
+ (int)Math.Ceiling(scale * ((srcX1 / srcTexture.Info.SamplesInX) - srcTexture.Info.Width)),
+ (int)Math.Ceiling(scale * ((srcY1 / srcTexture.Info.SamplesInY) + 1)),
+ (int)Math.Ceiling(scale * ((srcX2 / srcTexture.Info.SamplesInX) - srcTexture.Info.Width)),
+ (int)Math.Ceiling(scale * ((srcY2 / srcTexture.Info.SamplesInY) + 1)));
srcTexture.HostTexture.CopyTo(dstTexture.HostTexture, srcRegion, dstRegion, linearFilter);
}
diff --git a/Ryujinx.Graphics.Gpu/Engine/Methods.cs b/Ryujinx.Graphics.Gpu/Engine/Methods.cs
index af32c6bc1..5677c8a05 100644
--- a/Ryujinx.Graphics.Gpu/Engine/Methods.cs
+++ b/Ryujinx.Graphics.Gpu/Engine/Methods.cs
@@ -313,7 +313,8 @@ namespace Ryujinx.Graphics.Gpu.Engine
///
/// Current GPU state
/// Use draw buffers information from render target control register
- private void UpdateRenderTargetState(GpuState state, bool useControl)
+ /// If this is not -1, it indicates that only the given indexed target will be used.
+ private void UpdateRenderTargetState(GpuState state, bool useControl, int singleUse = -1)
{
var rtControl = state.Get(MethodOffset.RtControl);
@@ -324,6 +325,8 @@ namespace Ryujinx.Graphics.Gpu.Engine
int samplesInX = msaaMode.SamplesInX();
int samplesInY = msaaMode.SamplesInY();
+ bool changedScale = false;
+
for (int index = 0; index < Constants.TotalRenderTargets; index++)
{
int rtIndex = useControl ? rtControl.UnpackPermutationIndex(index) : index;
@@ -332,14 +335,14 @@ namespace Ryujinx.Graphics.Gpu.Engine
if (index >= count || !IsRtEnabled(colorState))
{
- TextureManager.SetRenderTargetColor(index, null);
+ changedScale |= TextureManager.SetRenderTargetColor(index, null);
continue;
}
Texture color = TextureManager.FindOrCreateTexture(colorState, samplesInX, samplesInY);
- TextureManager.SetRenderTargetColor(index, color);
+ changedScale |= TextureManager.SetRenderTargetColor(index, color);
if (color != null)
{
@@ -359,7 +362,16 @@ namespace Ryujinx.Graphics.Gpu.Engine
depthStencil = TextureManager.FindOrCreateTexture(dsState, dsSize, samplesInX, samplesInY);
}
- TextureManager.SetRenderTargetDepthStencil(depthStencil);
+ changedScale |= TextureManager.SetRenderTargetDepthStencil(depthStencil);
+
+ if (changedScale)
+ {
+ TextureManager.UpdateRenderTargetScale(singleUse);
+ _context.Renderer.Pipeline.SetRenderTargetScale(TextureManager.RenderTargetScale);
+
+ UpdateViewportTransform(state);
+ UpdateScissorState(state);
+ }
if (depthStencil != null)
{
@@ -394,7 +406,21 @@ namespace Ryujinx.Graphics.Gpu.Engine
if (enable)
{
- _context.Renderer.Pipeline.SetScissor(index, scissor.X1, scissor.Y1, scissor.X2 - scissor.X1, scissor.Y2 - scissor.Y1);
+ int x = scissor.X1;
+ int y = scissor.Y1;
+ int width = scissor.X2 - x;
+ int height = scissor.Y2 - y;
+
+ float scale = TextureManager.RenderTargetScale;
+ if (scale != 1f)
+ {
+ x = (int)(x * scale);
+ y = (int)(y * scale);
+ width = (int)Math.Ceiling(width * scale);
+ height = (int)Math.Ceiling(height * scale);
+ }
+
+ _context.Renderer.Pipeline.SetScissor(index, x, y, width, height);
}
}
}
@@ -460,6 +486,15 @@ namespace Ryujinx.Graphics.Gpu.Engine
float width = MathF.Abs(transform.ScaleX) * 2;
float height = MathF.Abs(transform.ScaleY) * 2;
+ float scale = TextureManager.RenderTargetScale;
+ if (scale != 1f)
+ {
+ x *= scale;
+ y *= scale;
+ width *= scale;
+ height *= scale;
+ }
+
RectangleF region = new RectangleF(x, y, width, height);
ViewportSwizzle swizzleX = transform.UnpackSwizzleX();
@@ -909,11 +944,11 @@ namespace Ryujinx.Graphics.Gpu.Engine
if (descriptor.IsBindless)
{
- textureBindings[index] = new TextureBindingInfo(target, descriptor.CbufSlot, descriptor.CbufOffset);
+ textureBindings[index] = new TextureBindingInfo(target, descriptor.CbufSlot, descriptor.CbufOffset, descriptor.Flags);
}
else
{
- textureBindings[index] = new TextureBindingInfo(target, descriptor.HandleIndex);
+ textureBindings[index] = new TextureBindingInfo(target, descriptor.HandleIndex, descriptor.Flags);
}
}
@@ -927,7 +962,7 @@ namespace Ryujinx.Graphics.Gpu.Engine
Target target = GetTarget(descriptor.Type);
- imageBindings[index] = new TextureBindingInfo(target, descriptor.HandleIndex);
+ imageBindings[index] = new TextureBindingInfo(target, descriptor.HandleIndex, descriptor.Flags);
}
TextureManager.SetGraphicsImages(stage, imageBindings);
diff --git a/Ryujinx.Graphics.Gpu/GraphicsConfig.cs b/Ryujinx.Graphics.Gpu/GraphicsConfig.cs
index 4bda7c19c..44b2b5e97 100644
--- a/Ryujinx.Graphics.Gpu/GraphicsConfig.cs
+++ b/Ryujinx.Graphics.Gpu/GraphicsConfig.cs
@@ -5,6 +5,11 @@ namespace Ryujinx.Graphics.Gpu
///
public static class GraphicsConfig
{
+ ///
+ /// Resolution scale.
+ ///
+ public static float ResScale = 1f;
+
///
/// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide.
///
diff --git a/Ryujinx.Graphics.Gpu/Image/Texture.cs b/Ryujinx.Graphics.Gpu/Image/Texture.cs
index 0f952ffd4..87c409a08 100644
--- a/Ryujinx.Graphics.Gpu/Image/Texture.cs
+++ b/Ryujinx.Graphics.Gpu/Image/Texture.cs
@@ -29,10 +29,20 @@ namespace Ryujinx.Graphics.Gpu.Image
///
public TextureInfo Info { get; private set; }
+ ///
+ /// Host scale factor.
+ ///
+ public float ScaleFactor { get; private set; }
+
+ ///
+ /// Upscaling mode. Informs if a texture is scaled, or is eligible for scaling.
+ ///
+ public TextureScaleMode ScaleMode { get; private set; }
+
private int _depth;
private int _layers;
- private readonly int _firstLayer;
- private readonly int _firstLevel;
+ private int _firstLayer;
+ private int _firstLevel;
private bool _hasData;
@@ -92,18 +102,25 @@ namespace Ryujinx.Graphics.Gpu.Image
/// Size information of the texture
/// The first layer of the texture, or 0 if the texture has no parent
/// The first mipmap level of the texture, or 0 if the texture has no parent
+ /// The floating point scale factor to initialize with
+ /// The scale mode to initialize with
private Texture(
- GpuContext context,
- TextureInfo info,
- SizeInfo sizeInfo,
- int firstLayer,
- int firstLevel)
+ GpuContext context,
+ TextureInfo info,
+ SizeInfo sizeInfo,
+ int firstLayer,
+ int firstLevel,
+ float scaleFactor,
+ TextureScaleMode scaleMode)
{
InitializeTexture(context, info, sizeInfo);
_firstLayer = firstLayer;
_firstLevel = firstLevel;
+ ScaleFactor = scaleFactor;
+ ScaleMode = scaleMode;
+
_hasData = true;
}
@@ -113,13 +130,23 @@ namespace Ryujinx.Graphics.Gpu.Image
/// GPU context that the texture belongs to
/// Texture information
/// Size information of the texture
- public Texture(GpuContext context, TextureInfo info, SizeInfo sizeInfo)
+ /// The scale mode to initialize with. If scaled, the texture's data is loaded immediately and scaled up
+ public Texture(GpuContext context, TextureInfo info, SizeInfo sizeInfo, TextureScaleMode scaleMode)
{
+ ScaleFactor = 1f; // Texture is first loaded at scale 1x.
+ ScaleMode = scaleMode;
+
InitializeTexture(context, info, sizeInfo);
TextureCreateInfo createInfo = TextureManager.GetCreateInfo(info, context.Capabilities);
- HostTexture = _context.Renderer.CreateTexture(createInfo);
+ HostTexture = _context.Renderer.CreateTexture(createInfo, ScaleFactor);
+
+ if (scaleMode == TextureScaleMode.Scaled)
+ {
+ SynchronizeMemory(); // Load the data and then scale it up.
+ SetScale(GraphicsConfig.ResScale);
+ }
}
///
@@ -162,7 +189,9 @@ namespace Ryujinx.Graphics.Gpu.Image
info,
sizeInfo,
_firstLayer + firstLayer,
- _firstLevel + firstLevel);
+ _firstLevel + firstLevel,
+ ScaleFactor,
+ ScaleMode);
TextureCreateInfo createInfo = TextureManager.GetCreateInfo(info, _context.Capabilities);
@@ -282,7 +311,7 @@ namespace Ryujinx.Graphics.Gpu.Image
}
else
{
- ITexture newStorage = _context.Renderer.CreateTexture(createInfo);
+ ITexture newStorage = _context.Renderer.CreateTexture(createInfo, ScaleFactor);
HostTexture.CopyTo(newStorage, 0, 0);
@@ -290,6 +319,149 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
+ ///
+ /// Blacklists this texture from being scaled. Resets its scale to 1 if needed.
+ ///
+ public void BlacklistScale()
+ {
+ ScaleMode = TextureScaleMode.Blacklisted;
+ SetScale(1f);
+ }
+
+ ///
+ /// Propagates the scale between this texture and another to ensure they have the same scale.
+ /// If one texture is blacklisted from scaling, the other will become blacklisted too.
+ ///
+ /// The other texture
+ public void PropagateScale(Texture other)
+ {
+ if (other.ScaleMode == TextureScaleMode.Blacklisted || ScaleMode == TextureScaleMode.Blacklisted)
+ {
+ BlacklistScale();
+ other.BlacklistScale();
+ }
+ else
+ {
+ // Prefer the configured scale if present. If not, prefer the max.
+ float targetScale = GraphicsConfig.ResScale;
+ float sharedScale = (ScaleFactor == targetScale || other.ScaleFactor == targetScale) ? targetScale : Math.Max(ScaleFactor, other.ScaleFactor);
+
+ SetScale(sharedScale);
+ other.SetScale(sharedScale);
+ }
+ }
+
+ ///
+ /// Helper method for copying our Texture2DArray texture to the given target, with scaling.
+ /// This creates temporary views for each array layer on both textures, copying each one at a time.
+ ///
+ /// The texture array to copy to
+ private void CopyArrayScaled(ITexture target)
+ {
+ TextureInfo viewInfo = new TextureInfo(
+ Info.Address,
+ Info.Width,
+ Info.Height,
+ 1,
+ Info.Levels,
+ Info.SamplesInX,
+ Info.SamplesInY,
+ Info.Stride,
+ Info.IsLinear,
+ Info.GobBlocksInY,
+ Info.GobBlocksInZ,
+ Info.GobBlocksInTileX,
+ Target.Texture2D,
+ Info.FormatInfo,
+ Info.DepthStencilMode,
+ Info.SwizzleR,
+ Info.SwizzleG,
+ Info.SwizzleB,
+ Info.SwizzleA);
+
+ TextureCreateInfo createInfo = TextureManager.GetCreateInfo(viewInfo, _context.Capabilities);
+
+ for (int i = 0; i < Info.DepthOrLayers; i++)
+ {
+ ITexture from = HostTexture.CreateView(createInfo, i, 0);
+ ITexture to = target.CreateView(createInfo, i, 0);
+
+ from.CopyTo(to, new Extents2D(0, 0, from.Width, from.Height), new Extents2D(0, 0, to.Width, to.Height), true);
+
+ from.Dispose();
+ to.Dispose();
+ }
+ }
+
+ ///
+ /// Sets the Scale Factor on this texture, and immediately recreates it at the correct size.
+ /// When a texture is resized, a scaled copy is performed from the old texture to the new one, to ensure no data is lost.
+ /// If scale is equivalent, this only propagates the blacklisted/scaled mode.
+ /// If called on a view, its storage is resized instead.
+ /// When resizing storage, all texture views are recreated.
+ ///
+ /// The new scale factor for this texture
+ public void SetScale(float scale)
+ {
+ TextureScaleMode newScaleMode = ScaleMode == TextureScaleMode.Blacklisted ? ScaleMode : TextureScaleMode.Scaled;
+
+ if (_viewStorage != this)
+ {
+ _viewStorage.ScaleMode = newScaleMode;
+ _viewStorage.SetScale(scale);
+ return;
+ }
+
+ if (ScaleFactor != scale)
+ {
+ Logger.PrintDebug(LogClass.Gpu, $"Rescaling {Info.Width}x{Info.Height} {Info.FormatInfo.Format.ToString()} to ({ScaleFactor} to {scale}). ");
+ TextureCreateInfo createInfo = TextureManager.GetCreateInfo(Info, _context.Capabilities);
+
+ ScaleFactor = scale;
+
+ ITexture newStorage = _context.Renderer.CreateTexture(createInfo, ScaleFactor);
+
+ if (Info.Target == Target.Texture2DArray)
+ {
+ CopyArrayScaled(newStorage);
+ }
+ else
+ {
+ HostTexture.CopyTo(newStorage, new Extents2D(0, 0, HostTexture.Width, HostTexture.Height), new Extents2D(0, 0, newStorage.Width, newStorage.Height), true);
+ }
+
+ Logger.PrintDebug(LogClass.Gpu, $" Copy performed: {HostTexture.Width}x{HostTexture.Height} to {newStorage.Width}x{newStorage.Height}");
+
+ ReplaceStorage(newStorage);
+
+ // All views must be recreated against the new storage.
+
+ foreach (var view in _views)
+ {
+ Logger.PrintDebug(LogClass.Gpu, $" Recreating view {Info.Width}x{Info.Height} {Info.FormatInfo.Format.ToString()}.");
+ view.ScaleFactor = scale;
+
+ TextureCreateInfo viewCreateInfo = TextureManager.GetCreateInfo(view.Info, _context.Capabilities);
+
+ ITexture newView = HostTexture.CreateView(viewCreateInfo, view._firstLayer - _firstLayer, view._firstLevel - _firstLevel);
+
+ view.ReplaceStorage(newView);
+
+ view.ScaleMode = newScaleMode;
+ }
+ }
+
+ if (ScaleMode != newScaleMode)
+ {
+ ScaleMode = newScaleMode;
+
+ foreach (var view in _views)
+ {
+ view.ScaleMode = newScaleMode;
+ }
+ }
+ }
+
///
/// Synchronizes guest and host memory.
/// This will overwrite the texture data with the texture data on the guest memory, if a CPU
@@ -310,9 +482,14 @@ namespace Ryujinx.Graphics.Gpu.Image
int modifiedCount = _context.PhysicalMemory.QueryModified(Address, Size, ResourceName.Texture, _modifiedRanges);
- if (modifiedCount == 0 && _hasData)
+ if (_hasData)
{
- return;
+ if (modifiedCount == 0)
+ {
+ return;
+ }
+
+ BlacklistScale();
}
ReadOnlySpan data = _context.PhysicalMemory.GetSpan(Address, (int)Size);
@@ -432,6 +609,7 @@ namespace Ryujinx.Graphics.Gpu.Image
///
public void Flush()
{
+ BlacklistScale();
_context.PhysicalMemory.Write(Address, GetTextureDataFromGpu());
}
@@ -445,6 +623,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// Host texture data
private Span GetTextureDataFromGpu()
{
+ BlacklistScale();
Span data = HostTexture.GetData();
if (Info.IsLinear)
@@ -980,10 +1159,14 @@ namespace Ryujinx.Graphics.Gpu.Image
/// The parent texture
/// The new view texture information
/// The new host texture
- public void ReplaceView(Texture parent, TextureInfo info, ITexture hostTexture)
+ /// The first layer of the view
+ /// The first level of the view
+ public void ReplaceView(Texture parent, TextureInfo info, ITexture hostTexture, int firstLayer, int firstLevel)
{
ReplaceStorage(hostTexture);
+ _firstLayer = parent._firstLayer + firstLayer;
+ _firstLevel = parent._firstLevel + firstLevel;
parent._viewStorage.AddView(this);
SetInfo(info);
@@ -1075,7 +1258,7 @@ namespace Ryujinx.Graphics.Gpu.Image
// already deleted (views count is 0).
if (_referenceCount == 0 && _views.Count == 0)
{
- DisposeTextures();
+ Dispose();
}
}
@@ -1088,8 +1271,6 @@ namespace Ryujinx.Graphics.Gpu.Image
_arrayViewTexture?.Dispose();
_arrayViewTexture = null;
-
- Disposed?.Invoke(this);
}
///
@@ -1098,6 +1279,8 @@ namespace Ryujinx.Graphics.Gpu.Image
public void Dispose()
{
DisposeTextures();
+
+ Disposed?.Invoke(this);
}
}
}
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs b/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs
index b4793f58f..175f8863e 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs
@@ -1,4 +1,5 @@
using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Shader;
namespace Ryujinx.Graphics.Gpu.Image
{
@@ -37,12 +38,18 @@ namespace Ryujinx.Graphics.Gpu.Image
///
public int CbufOffset { get; }
+ ///
+ /// Flags from the texture descriptor that indicate how the texture is used.
+ ///
+ public TextureUsageFlags Flags { get; }
+
///
/// Constructs the texture binding information structure.
///
/// The shader sampler target type
/// The shader texture handle (read index into the texture constant buffer)
- public TextureBindingInfo(Target target, int handle)
+ /// The texture's usage flags, indicating how it is used in the shader
+ public TextureBindingInfo(Target target, int handle, TextureUsageFlags flags)
{
Target = target;
Handle = handle;
@@ -51,6 +58,8 @@ namespace Ryujinx.Graphics.Gpu.Image
CbufSlot = 0;
CbufOffset = 0;
+
+ Flags = flags;
}
///
@@ -59,7 +68,8 @@ namespace Ryujinx.Graphics.Gpu.Image
/// The shader sampler target type
/// Constant buffer slot where the bindless texture handle is located
/// Constant buffer offset of the bindless texture handle
- public TextureBindingInfo(Target target, int cbufSlot, int cbufOffset)
+ /// The texture's usage flags, indicating how it is used in the shader
+ public TextureBindingInfo(Target target, int cbufSlot, int cbufOffset, TextureUsageFlags flags)
{
Target = target;
Handle = 0;
@@ -68,6 +78,8 @@ namespace Ryujinx.Graphics.Gpu.Image
CbufSlot = cbufSlot;
CbufOffset = cbufOffset;
+
+ Flags = flags;
}
}
}
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs b/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs
index 672544409..87b0f444f 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs
@@ -174,6 +174,8 @@ namespace Ryujinx.Graphics.Gpu.Image
return;
}
+ bool changed = false;
+
for (int index = 0; index < _textureBindings[stageIndex].Length; index++)
{
TextureBindingInfo binding = _textureBindings[stageIndex][index];
@@ -216,6 +218,11 @@ namespace Ryujinx.Graphics.Gpu.Image
Texture texture = pool.Get(textureId);
+ if ((binding.Flags & TextureUsageFlags.ResScaleUnsupported) != 0)
+ {
+ texture?.BlacklistScale();
+ }
+
ITexture hostTexture = texture?.GetTargetTexture(binding.Target);
if (_textureState[stageIndex][index].Texture != hostTexture || _rebind)
@@ -223,6 +230,8 @@ namespace Ryujinx.Graphics.Gpu.Image
_textureState[stageIndex][index].Texture = hostTexture;
_context.Renderer.Pipeline.SetTexture(index, stage, hostTexture);
+
+ changed = true;
}
if (hostTexture != null && texture.Info.Target == Target.TextureBuffer)
@@ -244,6 +253,11 @@ namespace Ryujinx.Graphics.Gpu.Image
_context.Renderer.Pipeline.SetSampler(index, stage, hostSampler);
}
}
+
+ if (changed)
+ {
+ _context.Renderer.Pipeline.UpdateRenderScale(stage, _textureBindings[stageIndex].Length);
+ }
}
///
@@ -269,6 +283,11 @@ namespace Ryujinx.Graphics.Gpu.Image
Texture texture = pool.Get(textureId);
+ if ((binding.Flags & TextureUsageFlags.ResScaleUnsupported) != 0)
+ {
+ texture?.BlacklistScale();
+ }
+
ITexture hostTexture = texture?.GetTargetTexture(binding.Target);
if (_imageState[stageIndex][index].Texture != hostTexture || _rebind)
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/Ryujinx.Graphics.Gpu/Image/TextureManager.cs
index c0eeb0680..ccd56ae21 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureManager.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureManager.cs
@@ -39,6 +39,11 @@ namespace Ryujinx.Graphics.Gpu.Image
private readonly HashSet _modified;
private readonly HashSet _modifiedLinear;
+ ///
+ /// The scaling factor applied to all currently bound render targets.
+ ///
+ public float RenderTargetScale { get; private set; } = 1f;
+
///
/// Constructs a new instance of the texture manager.
///
@@ -169,18 +174,112 @@ namespace Ryujinx.Graphics.Gpu.Image
///
/// The index of the color buffer to set (up to 8)
/// The color buffer texture
- public void SetRenderTargetColor(int index, Texture color)
+ /// True if render target scale must be updated.
+ public bool SetRenderTargetColor(int index, Texture color)
{
+ bool hasValue = color != null;
+ bool changesScale = (hasValue != (_rtColors[index] != null)) || (hasValue && RenderTargetScale != color.ScaleFactor);
_rtColors[index] = color;
+
+ return changesScale || (hasValue && color.ScaleMode != TextureScaleMode.Blacklisted && color.ScaleFactor != GraphicsConfig.ResScale);
+ }
+
+ ///
+ /// Updates the Render Target scale, given the currently bound render targets.
+ /// This will update scale to match the configured scale, scale textures that are eligible but not scaled,
+ /// and propagate blacklisted status from one texture to the ones bound with it.
+ ///
+ /// If this is not -1, it indicates that only the given indexed target will be used.
+ public void UpdateRenderTargetScale(int singleUse)
+ {
+ // Make sure all scales for render targets are at the highest they should be. Blacklisted targets should propagate their scale to the other targets.
+ bool mismatch = false;
+ bool blacklisted = false;
+ bool hasUpscaled = false;
+ float targetScale = GraphicsConfig.ResScale;
+
+ void ConsiderTarget(Texture target)
+ {
+ if (target == null) return;
+ float scale = target.ScaleFactor;
+
+ switch (target.ScaleMode)
+ {
+ case TextureScaleMode.Blacklisted:
+ mismatch |= scale != 1f;
+ blacklisted = true;
+ break;
+ case TextureScaleMode.Eligible:
+ mismatch = true; // We must make a decision.
+ break;
+ case TextureScaleMode.Scaled:
+ hasUpscaled = true;
+ mismatch |= scale != targetScale; // If the target scale has changed, reset the scale for all targets.
+ break;
+ }
+ }
+
+ if (singleUse != -1)
+ {
+ // If only one target is in use (by a clear, for example) the others do not need to be checked for mismatching scale.
+ ConsiderTarget(_rtColors[singleUse]);
+ }
+ else
+ {
+ foreach (Texture color in _rtColors)
+ {
+ ConsiderTarget(color);
+ }
+ }
+
+ ConsiderTarget(_rtDepthStencil);
+
+ mismatch |= blacklisted && hasUpscaled;
+
+ if (blacklisted)
+ {
+ targetScale = 1f;
+ }
+
+ if (mismatch)
+ {
+ if (blacklisted)
+ {
+ // Propagate the blacklisted state to the other textures.
+ foreach (Texture color in _rtColors)
+ {
+ color?.BlacklistScale();
+ }
+
+ _rtDepthStencil?.BlacklistScale();
+ }
+ else
+ {
+ // Set the scale of the other textures.
+ foreach (Texture color in _rtColors)
+ {
+ color?.SetScale(targetScale);
+ }
+
+ _rtDepthStencil?.SetScale(targetScale);
+ }
+ }
+
+ RenderTargetScale = targetScale;
}
///
/// Sets the render target depth-stencil buffer.
///
/// The depth-stencil buffer texture
- public void SetRenderTargetDepthStencil(Texture depthStencil)
+ /// True if render target scale must be updated.
+ public bool SetRenderTargetDepthStencil(Texture depthStencil)
{
+ bool hasValue = depthStencil != null;
+ bool changesScale = (hasValue != (_rtDepthStencil != null)) || (hasValue && RenderTargetScale != depthStencil.ScaleFactor);
_rtDepthStencil = depthStencil;
+
+ return changesScale || (hasValue && depthStencil.ScaleMode != TextureScaleMode.Blacklisted && depthStencil.ScaleFactor != GraphicsConfig.ResScale);
}
///
@@ -262,12 +361,59 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
+ ///
+ /// Determines if a given texture is eligible for upscaling from its info.
+ ///
+ /// The texture info to check
+ /// True if eligible
+ public bool IsUpscaleCompatible(TextureInfo info)
+ {
+ return (info.Target == Target.Texture2D || info.Target == Target.Texture2DArray) && info.Levels == 1 && !info.FormatInfo.IsCompressed && UpscaleSafeMode(info);
+ }
+
+ ///
+ /// Determines if a given texture is "safe" for upscaling from its info.
+ /// Note that this is different from being compatible - this elilinates targets that would have detrimental effects when scaled.
+ ///
+ /// The texture info to check
+ /// True if safe
+ public bool UpscaleSafeMode(TextureInfo info)
+ {
+ // While upscaling works for all targets defined by IsUpscaleCompatible, we additionally blacklist targets here that
+ // may have undesirable results (upscaling blur textures) or simply waste GPU resources (upscaling texture atlas).
+
+ if (!(info.FormatInfo.Format.IsDepthOrStencil() || info.FormatInfo.Format.HasOneComponent()))
+ {
+ // Discount square textures that aren't depth-stencil like. (excludes game textures, cubemap faces, most 3D texture LUT, texture atlas)
+ // Detect if the texture is possibly square. Widths may be aligned, so to remove the uncertainty we align both the width and height.
+
+ int widthAlignment = (info.IsLinear ? 32 : 64) / info.FormatInfo.BytesPerPixel;
+
+ bool possiblySquare = BitUtils.AlignUp(info.Width, widthAlignment) == BitUtils.AlignUp(info.Height, widthAlignment);
+
+ if (possiblySquare)
+ {
+ return false;
+ }
+ }
+
+ int aspect = (int)Math.Round((info.Width / (float)info.Height) * 9);
+ if (aspect == 16 && info.Height < 360)
+ {
+ // Targets that are roughly 16:9 can only be rescaled if they're equal to or above 360p. (excludes blur and bloom textures)
+ return false;
+ }
+
+ return true;
+ }
+
///
/// Tries to find an existing texture, or create a new one if not found.
///
/// Copy texture to find or create
+ /// Indicates if the texture should be scaled from the start
/// The texture
- public Texture FindOrCreateTexture(CopyTexture copyTexture)
+ public Texture FindOrCreateTexture(CopyTexture copyTexture, bool preferScaling = true)
{
ulong address = _context.MemoryManager.Translate(copyTexture.Address.Pack());
@@ -308,7 +454,14 @@ namespace Ryujinx.Graphics.Gpu.Image
Target.Texture2D,
formatInfo);
- Texture texture = FindOrCreateTexture(info, TextureSearchFlags.IgnoreMs);
+ TextureSearchFlags flags = TextureSearchFlags.IgnoreMs;
+
+ if (preferScaling)
+ {
+ flags |= TextureSearchFlags.WithUpscale;
+ }
+
+ Texture texture = FindOrCreateTexture(info, flags);
texture.SynchronizeMemory();
@@ -391,7 +544,7 @@ namespace Ryujinx.Graphics.Gpu.Image
target,
formatInfo);
- Texture texture = FindOrCreateTexture(info);
+ Texture texture = FindOrCreateTexture(info, TextureSearchFlags.WithUpscale);
texture.SynchronizeMemory();
@@ -440,7 +593,7 @@ namespace Ryujinx.Graphics.Gpu.Image
target,
formatInfo);
- Texture texture = FindOrCreateTexture(info);
+ Texture texture = FindOrCreateTexture(info, TextureSearchFlags.WithUpscale);
texture.SynchronizeMemory();
@@ -457,6 +610,14 @@ namespace Ryujinx.Graphics.Gpu.Image
{
bool isSamplerTexture = (flags & TextureSearchFlags.Sampler) != 0;
+ bool isScalable = IsUpscaleCompatible(info);
+
+ TextureScaleMode scaleMode = TextureScaleMode.Blacklisted;
+ if (isScalable)
+ {
+ scaleMode = (flags & TextureSearchFlags.WithUpscale) != 0 ? TextureScaleMode.Scaled : TextureScaleMode.Eligible;
+ }
+
// Try to find a perfect texture match, with the same address and parameters.
int sameAddressOverlapsCount = _textures.FindOverlaps(info.Address, ref _textureOverlaps);
@@ -556,7 +717,7 @@ namespace Ryujinx.Graphics.Gpu.Image
// No match, create a new texture.
if (texture == null)
{
- texture = new Texture(_context, info, sizeInfo);
+ texture = new Texture(_context, info, sizeInfo, scaleMode);
// We need to synchronize before copying the old view data to the texture,
// otherwise the copied data would be overwritten by a future synchronization.
@@ -572,6 +733,14 @@ namespace Ryujinx.Graphics.Gpu.Image
TextureCreateInfo createInfo = GetCreateInfo(overlapInfo, _context.Capabilities);
+ if (texture.ScaleFactor != overlap.ScaleFactor)
+ {
+ // A bit tricky, our new texture may need to contain an existing texture that is upscaled, but isn't itself.
+ // In that case, we prefer the higher scale only if our format is render-target-like, otherwise we scale the view down before copy.
+
+ texture.PropagateScale(overlap);
+ }
+
ITexture newView = texture.HostTexture.CreateView(createInfo, firstLayer, firstLevel);
overlap.HostTexture.CopyTo(newView, 0, 0);
@@ -583,7 +752,7 @@ namespace Ryujinx.Graphics.Gpu.Image
CacheTextureModified(texture);
}
- overlap.ReplaceView(texture, overlapInfo, newView);
+ overlap.ReplaceView(texture, overlapInfo, newView, firstLayer, firstLevel);
}
}
@@ -602,6 +771,8 @@ namespace Ryujinx.Graphics.Gpu.Image
out int firstLayer,
out int firstLevel))
{
+ overlap.BlacklistScale();
+
overlap.HostTexture.CopyTo(texture.HostTexture, firstLayer, firstLevel);
if (IsTextureModified(overlap))
diff --git a/Ryujinx.Graphics.Gpu/Image/TexturePool.cs b/Ryujinx.Graphics.Gpu/Image/TexturePool.cs
index 639fa69da..1494a1425 100644
--- a/Ryujinx.Graphics.Gpu/Image/TexturePool.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TexturePool.cs
@@ -174,7 +174,7 @@ namespace Ryujinx.Graphics.Gpu.Image
swizzleB,
swizzleA);
- if (IsDepthStencil(formatInfo.Format))
+ if (formatInfo.Format.IsDepthOrStencil())
{
swizzleR = SwizzleComponent.Red;
swizzleG = SwizzleComponent.Red;
@@ -263,26 +263,6 @@ namespace Ryujinx.Graphics.Gpu.Image
component == SwizzleComponent.Green;
}
- ///
- /// Checks if the texture format is a depth, stencil or depth-stencil format.
- ///
- /// Texture format
- /// True if the format is a depth, stencil or depth-stencil format, false otherwise
- private static bool IsDepthStencil(Format format)
- {
- switch (format)
- {
- case Format.D16Unorm:
- case Format.D24UnormS8Uint:
- case Format.D24X8Unorm:
- case Format.D32Float:
- case Format.D32FloatS8Uint:
- return true;
- }
-
- return false;
- }
-
///
/// Decrements the reference count of the texture.
/// This indicates that the texture pool is not using it anymore.
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureScaleMode.cs b/Ryujinx.Graphics.Gpu/Image/TextureScaleMode.cs
new file mode 100644
index 000000000..2c9e431dc
--- /dev/null
+++ b/Ryujinx.Graphics.Gpu/Image/TextureScaleMode.cs
@@ -0,0 +1,14 @@
+namespace Ryujinx.Graphics.Gpu.Image
+{
+ ///
+ /// The scale mode for a given texture.
+ /// Blacklisted textures cannot be scaled, Eligible textures have not been scaled yet,
+ /// and Scaled textures have been scaled already.
+ ///
+ enum TextureScaleMode
+ {
+ Eligible = 0,
+ Scaled = 1,
+ Blacklisted = 2
+ }
+}
diff --git a/Ryujinx.Graphics.Gpu/Image/TextureSearchFlags.cs b/Ryujinx.Graphics.Gpu/Image/TextureSearchFlags.cs
index daf726f1d..33ac775c2 100644
--- a/Ryujinx.Graphics.Gpu/Image/TextureSearchFlags.cs
+++ b/Ryujinx.Graphics.Gpu/Image/TextureSearchFlags.cs
@@ -11,6 +11,7 @@ namespace Ryujinx.Graphics.Gpu.Image
None = 0,
IgnoreMs = 1 << 0,
Strict = 1 << 1 | Sampler,
- Sampler = 1 << 2
+ Sampler = 1 << 2,
+ WithUpscale = 1 << 3
}
}
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Gpu/Window.cs b/Ryujinx.Graphics.Gpu/Window.cs
index e9f10e812..10ee74bec 100644
--- a/Ryujinx.Graphics.Gpu/Window.cs
+++ b/Ryujinx.Graphics.Gpu/Window.cs
@@ -146,7 +146,7 @@ namespace Ryujinx.Graphics.Gpu
{
pt.AcquireCallback(_context, pt.UserObj);
- Texture texture = _context.Methods.TextureManager.FindOrCreateTexture(pt.Info);
+ Texture texture = _context.Methods.TextureManager.FindOrCreateTexture(pt.Info, TextureSearchFlags.WithUpscale);
texture.SynchronizeMemory();
diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureBase.cs b/Ryujinx.Graphics.OpenGL/Image/TextureBase.cs
index a4209ea15..dfb81642c 100644
--- a/Ryujinx.Graphics.OpenGL/Image/TextureBase.cs
+++ b/Ryujinx.Graphics.OpenGL/Image/TextureBase.cs
@@ -1,5 +1,6 @@
using OpenTK.Graphics.OpenGL;
using Ryujinx.Graphics.GAL;
+using System;
namespace Ryujinx.Graphics.OpenGL.Image
{
@@ -9,15 +10,19 @@ namespace Ryujinx.Graphics.OpenGL.Image
protected TextureCreateInfo Info { get; }
- public int Width => Info.Width;
- public int Height => Info.Height;
+ public int Width { get; }
+ public int Height { get; }
+ public float ScaleFactor { get; }
public Target Target => Info.Target;
public Format Format => Info.Format;
- public TextureBase(TextureCreateInfo info)
+ public TextureBase(TextureCreateInfo info, float scaleFactor = 1f)
{
Info = info;
+ Width = (int)Math.Ceiling(Info.Width * scaleFactor);
+ Height = (int)Math.Ceiling(Info.Height * scaleFactor);
+ ScaleFactor = scaleFactor;
Handle = GL.GenTexture();
}
diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs b/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs
index db9ff41c2..e89d5614c 100644
--- a/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs
+++ b/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs
@@ -33,6 +33,11 @@ namespace Ryujinx.Graphics.OpenGL.Image
ClearBufferMask mask = GetMask(src.Format);
+ if ((mask & (ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit)) != 0 || src.Format.IsInteger())
+ {
+ linearFilter = false;
+ }
+
BlitFramebufferFilter filter = linearFilter
? BlitFramebufferFilter.Linear
: BlitFramebufferFilter.Nearest;
@@ -55,6 +60,9 @@ namespace Ryujinx.Graphics.OpenGL.Image
mask,
filter);
+ Attach(FramebufferTarget.ReadFramebuffer, src.Format, 0);
+ Attach(FramebufferTarget.DrawFramebuffer, dst.Format, 0);
+
GL.BindFramebuffer(FramebufferTarget.ReadFramebuffer, oldReadFramebufferHandle);
GL.BindFramebuffer(FramebufferTarget.DrawFramebuffer, oldDrawFramebufferHandle);
diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureCopyUnscaled.cs b/Ryujinx.Graphics.OpenGL/Image/TextureCopyUnscaled.cs
index 284011385..02ae3b581 100644
--- a/Ryujinx.Graphics.OpenGL/Image/TextureCopyUnscaled.cs
+++ b/Ryujinx.Graphics.OpenGL/Image/TextureCopyUnscaled.cs
@@ -15,15 +15,16 @@ namespace Ryujinx.Graphics.OpenGL.Image
int srcLayer,
int dstLayer,
int srcLevel,
- int dstLevel)
+ int dstLevel,
+ float scaleFactor = 1f)
{
- int srcWidth = srcInfo.Width;
- int srcHeight = srcInfo.Height;
+ int srcWidth = (int)Math.Ceiling(srcInfo.Width * scaleFactor);
+ int srcHeight = (int)Math.Ceiling(srcInfo.Height * scaleFactor);
int srcDepth = srcInfo.GetDepthOrLayers();
int srcLevels = srcInfo.Levels;
- int dstWidth = dstInfo.Width;
- int dstHeight = dstInfo.Height;
+ int dstWidth = (int)Math.Ceiling(dstInfo.Width * scaleFactor);
+ int dstHeight = (int)Math.Ceiling(dstInfo.Height * scaleFactor);
int dstDepth = dstInfo.GetDepthOrLayers();
int dstLevels = dstInfo.Levels;
diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs b/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs
index baf8e65d0..4fc0a77fc 100644
--- a/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs
+++ b/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs
@@ -1,12 +1,14 @@
using OpenTK.Graphics.OpenGL;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
+using System;
namespace Ryujinx.Graphics.OpenGL.Image
{
class TextureStorage
{
public int Handle { get; private set; }
+ public float ScaleFactor { get; private set; }
public TextureCreateInfo Info { get; }
@@ -14,12 +16,13 @@ namespace Ryujinx.Graphics.OpenGL.Image
private int _viewsCount;
- public TextureStorage(Renderer renderer, TextureCreateInfo info)
+ public TextureStorage(Renderer renderer, TextureCreateInfo info, float scaleFactor)
{
_renderer = renderer;
Info = info;
Handle = GL.GenTexture();
+ ScaleFactor = scaleFactor;
CreateImmutableStorage();
}
@@ -32,6 +35,9 @@ namespace Ryujinx.Graphics.OpenGL.Image
GL.BindTexture(target, Handle);
+ int width = (int)Math.Ceiling(Info.Width * ScaleFactor);
+ int height = (int)Math.Ceiling(Info.Height * ScaleFactor);
+
FormatInfo format = FormatTable.GetFormatInfo(Info.Format);
SizedInternalFormat internalFormat;
@@ -52,7 +58,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
TextureTarget1d.Texture1D,
Info.Levels,
internalFormat,
- Info.Width);
+ width);
break;
case Target.Texture1DArray:
@@ -60,8 +66,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
TextureTarget2d.Texture1DArray,
Info.Levels,
internalFormat,
- Info.Width,
- Info.Height);
+ width,
+ height);
break;
case Target.Texture2D:
@@ -69,8 +75,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
TextureTarget2d.Texture2D,
Info.Levels,
internalFormat,
- Info.Width,
- Info.Height);
+ width,
+ height);
break;
case Target.Texture2DArray:
@@ -78,8 +84,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
TextureTarget3d.Texture2DArray,
Info.Levels,
internalFormat,
- Info.Width,
- Info.Height,
+ width,
+ height,
Info.Depth);
break;
@@ -88,8 +94,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
TextureTargetMultisample2d.Texture2DMultisample,
Info.Samples,
internalFormat,
- Info.Width,
- Info.Height,
+ width,
+ height,
true);
break;
@@ -98,8 +104,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
TextureTargetMultisample3d.Texture2DMultisampleArray,
Info.Samples,
internalFormat,
- Info.Width,
- Info.Height,
+ width,
+ height,
Info.Depth,
true);
break;
@@ -109,8 +115,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
TextureTarget3d.Texture3D,
Info.Levels,
internalFormat,
- Info.Width,
- Info.Height,
+ width,
+ height,
Info.Depth);
break;
@@ -119,8 +125,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
TextureTarget2d.TextureCubeMap,
Info.Levels,
internalFormat,
- Info.Width,
- Info.Height);
+ width,
+ height);
break;
case Target.CubemapArray:
@@ -128,8 +134,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
(TextureTarget3d)All.TextureCubeMapArray,
Info.Levels,
internalFormat,
- Info.Width,
- Info.Height,
+ width,
+ height,
Info.Depth);
break;
diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureView.cs b/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
index 0b24a2962..02353ffaa 100644
--- a/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
+++ b/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
@@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
TextureStorage parent,
TextureCreateInfo info,
int firstLayer,
- int firstLevel) : base(info)
+ int firstLevel) : base(info, parent.ScaleFactor)
{
_renderer = renderer;
_parent = parent;
@@ -101,7 +101,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
// So we emulate that here with a texture copy (see the first CopyTo overload).
// However right now it only does a single copy right after the view is created,
// so it doesn't work for all cases.
- TextureView emulatedView = (TextureView)_renderer.CreateTexture(info);
+ TextureView emulatedView = (TextureView)_renderer.CreateTexture(info, ScaleFactor);
emulatedView._emulatedViewParent = this;
@@ -122,10 +122,10 @@ namespace Ryujinx.Graphics.OpenGL.Image
{
if (_incompatibleFormatView == null)
{
- _incompatibleFormatView = (TextureView)_renderer.CreateTexture(Info);
+ _incompatibleFormatView = (TextureView)_renderer.CreateTexture(Info, ScaleFactor);
}
- TextureCopyUnscaled.Copy(_parent.Info, _incompatibleFormatView.Info, _parent.Handle, _incompatibleFormatView.Handle, FirstLayer, 0, FirstLevel, 0);
+ TextureCopyUnscaled.Copy(_parent.Info, _incompatibleFormatView.Info, _parent.Handle, _incompatibleFormatView.Handle, FirstLayer, 0, FirstLevel, 0, ScaleFactor);
return _incompatibleFormatView.Handle;
}
@@ -137,7 +137,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
{
if (_incompatibleFormatView != null)
{
- TextureCopyUnscaled.Copy(_incompatibleFormatView.Info, _parent.Info, _incompatibleFormatView.Handle, _parent.Handle, 0, FirstLayer, 0, FirstLevel);
+ TextureCopyUnscaled.Copy(_incompatibleFormatView.Info, _parent.Info, _incompatibleFormatView.Handle, _parent.Handle, 0, FirstLayer, 0, FirstLevel, ScaleFactor);
}
}
@@ -145,7 +145,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
{
TextureView destinationView = (TextureView)destination;
- TextureCopyUnscaled.Copy(Info, destinationView.Info, Handle, destinationView.Handle, 0, firstLayer, 0, firstLevel);
+ TextureCopyUnscaled.Copy(Info, destinationView.Info, Handle, destinationView.Handle, 0, firstLayer, 0, firstLevel, ScaleFactor);
if (destinationView._emulatedViewParent != null)
{
@@ -157,7 +157,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
0,
destinationView.FirstLayer,
0,
- destinationView.FirstLevel);
+ destinationView.FirstLevel,
+ ScaleFactor);
}
}
diff --git a/Ryujinx.Graphics.OpenGL/Pipeline.cs b/Ryujinx.Graphics.OpenGL/Pipeline.cs
index 6c511e093..62e5394e1 100644
--- a/Ryujinx.Graphics.OpenGL/Pipeline.cs
+++ b/Ryujinx.Graphics.OpenGL/Pipeline.cs
@@ -31,7 +31,12 @@ namespace Ryujinx.Graphics.OpenGL
private int _boundDrawFramebuffer;
private int _boundReadFramebuffer;
+ private float[] _fpRenderScale = new float[33];
+ private float[] _cpRenderScale = new float[32];
+
private TextureBase _unit0Texture;
+ private TextureBase _rtColor0Texture;
+ private TextureBase _rtDepthTexture;
private ClipOrigin _clipOrigin;
private ClipDepthMode _clipDepthMode;
@@ -54,6 +59,16 @@ namespace Ryujinx.Graphics.OpenGL
{
_componentMasks[index] = 0xf;
}
+
+ for (int index = 0; index < _fpRenderScale.Length; index++)
+ {
+ _fpRenderScale[index] = 1f;
+ }
+
+ for (int index = 0; index < _cpRenderScale.Length; index++)
+ {
+ _cpRenderScale[index] = 1f;
+ }
}
public void Barrier()
@@ -685,6 +700,8 @@ namespace Ryujinx.Graphics.OpenGL
{
_program = (Program)program;
_program.Bind();
+
+ SetRenderTargetScale(_fpRenderScale[0]);
}
public void SetRasterizerDiscard(bool discard)
@@ -701,6 +718,16 @@ namespace Ryujinx.Graphics.OpenGL
_rasterizerDiscard = discard;
}
+ public void SetRenderTargetScale(float scale)
+ {
+ _fpRenderScale[0] = scale;
+
+ if (_program != null && _program.FragmentRenderScaleUniform != -1)
+ {
+ GL.Uniform1(_program.FragmentRenderScaleUniform, 1, _fpRenderScale); // Just the first element.
+ }
+ }
+
public void SetRenderTargetColorMasks(ReadOnlySpan componentMasks)
{
for (int index = 0; index < componentMasks.Length; index++)
@@ -715,6 +742,9 @@ namespace Ryujinx.Graphics.OpenGL
{
EnsureFramebuffer();
+ _rtColor0Texture = (TextureBase)colors[0];
+ _rtDepthTexture = (TextureBase)depthStencil;
+
for (int index = 0; index < colors.Length; index++)
{
TextureView color = (TextureView)colors[index];
@@ -826,6 +856,37 @@ namespace Ryujinx.Graphics.OpenGL
{
((TextureBase)texture).Bind(unit);
}
+
+ // Update scale factor for bound textures.
+
+ switch (stage)
+ {
+ case ShaderStage.Fragment:
+ if (_program.FragmentRenderScaleUniform != -1)
+ {
+ // Only update and send sampled texture scales if the shader uses them.
+ bool interpolate = false;
+ float scale = texture.ScaleFactor;
+
+ if (scale != 1)
+ {
+ TextureBase activeTarget = _rtColor0Texture ?? _rtDepthTexture;
+
+ if (activeTarget != null && activeTarget.Width / (float)texture.Width == activeTarget.Height / (float)texture.Height)
+ {
+ // If the texture's size is a multiple of the sampler size, enable interpolation using gl_FragCoord. (helps "invent" new integer values between scaled pixels)
+ interpolate = true;
+ }
+ }
+
+ _fpRenderScale[index + 1] = interpolate ? -scale : scale;
+ }
+ break;
+
+ case ShaderStage.Compute:
+ _cpRenderScale[index] = texture.ScaleFactor;
+ break;
+ }
}
}
@@ -1089,5 +1150,28 @@ namespace Ryujinx.Graphics.OpenGL
_framebuffer?.Dispose();
_vertexArray?.Dispose();
}
+
+ public void UpdateRenderScale(ShaderStage stage, int textureCount)
+ {
+ if (_program != null)
+ {
+ switch (stage)
+ {
+ case ShaderStage.Fragment:
+ if (_program.FragmentRenderScaleUniform != -1)
+ {
+ GL.Uniform1(_program.FragmentRenderScaleUniform, textureCount + 1, _fpRenderScale);
+ }
+ break;
+
+ case ShaderStage.Compute:
+ if (_program.ComputeRenderScaleUniform != -1)
+ {
+ GL.Uniform1(_program.ComputeRenderScaleUniform, textureCount, _cpRenderScale);
+ }
+ break;
+ }
+ }
+ }
}
}
diff --git a/Ryujinx.Graphics.OpenGL/Program.cs b/Ryujinx.Graphics.OpenGL/Program.cs
index fe14e9a9d..8b4f6e242 100644
--- a/Ryujinx.Graphics.OpenGL/Program.cs
+++ b/Ryujinx.Graphics.OpenGL/Program.cs
@@ -21,6 +21,9 @@ namespace Ryujinx.Graphics.OpenGL
public int Handle { get; private set; }
+ public int FragmentRenderScaleUniform { get; }
+ public int ComputeRenderScaleUniform { get; }
+
public bool IsLinked { get; private set; }
private int[] _ubBindingPoints;
@@ -162,6 +165,9 @@ namespace Ryujinx.Graphics.OpenGL
imageUnit++;
}
}
+
+ FragmentRenderScaleUniform = GL.GetUniformLocation(Handle, "fp_renderScale");
+ ComputeRenderScaleUniform = GL.GetUniformLocation(Handle, "cp_renderScale");
}
public void Bind()
diff --git a/Ryujinx.Graphics.OpenGL/Renderer.cs b/Ryujinx.Graphics.OpenGL/Renderer.cs
index 49dba9cc9..cf90f81f5 100644
--- a/Ryujinx.Graphics.OpenGL/Renderer.cs
+++ b/Ryujinx.Graphics.OpenGL/Renderer.cs
@@ -54,9 +54,9 @@ namespace Ryujinx.Graphics.OpenGL
return new Sampler(info);
}
- public ITexture CreateTexture(TextureCreateInfo info)
+ public ITexture CreateTexture(TextureCreateInfo info, float scaleFactor)
{
- return info.Target == Target.TextureBuffer ? new TextureBuffer(info) : new TextureStorage(this, info).CreateDefaultView();
+ return info.Target == Target.TextureBuffer ? new TextureBuffer(info) : new TextureStorage(this, info, scaleFactor).CreateDefaultView();
}
public void DeleteBuffer(BufferHandle buffer)
diff --git a/Ryujinx.Graphics.OpenGL/Window.cs b/Ryujinx.Graphics.OpenGL/Window.cs
index 6da9e7155..b7dc37843 100644
--- a/Ryujinx.Graphics.OpenGL/Window.cs
+++ b/Ryujinx.Graphics.OpenGL/Window.cs
@@ -65,11 +65,12 @@ namespace Ryujinx.Graphics.OpenGL
GL.Clear(ClearBufferMask.ColorBufferBit);
int srcX0, srcX1, srcY0, srcY1;
+ float scale = view.ScaleFactor;
if (crop.Left == 0 && crop.Right == 0)
{
srcX0 = 0;
- srcX1 = view.Width;
+ srcX1 = (int)(view.Width / scale);
}
else
{
@@ -80,7 +81,7 @@ namespace Ryujinx.Graphics.OpenGL
if (crop.Top == 0 && crop.Bottom == 0)
{
srcY0 = 0;
- srcY1 = view.Height;
+ srcY1 = (int)(view.Height / scale);
}
else
{
@@ -88,6 +89,14 @@ namespace Ryujinx.Graphics.OpenGL
srcY1 = crop.Bottom;
}
+ if (scale != 1f)
+ {
+ srcX0 = (int)(srcX0 * scale);
+ srcY0 = (int)(srcY0 * scale);
+ srcX1 = (int)Math.Ceiling(srcX1 * scale);
+ srcY1 = (int)Math.Ceiling(srcY1 * scale);
+ }
+
float ratioX = MathF.Min(1f, (_height * (float)NativeWidth) / ((float)NativeHeight * _width));
float ratioY = MathF.Min(1f, (_width * (float)NativeHeight) / ((float)NativeWidth * _height));
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/CodeGenContext.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/CodeGenContext.cs
index 6bef8e6c2..91ab7ad52 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/CodeGenContext.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/CodeGenContext.cs
@@ -1,3 +1,5 @@
+using Ryujinx.Graphics.Shader.IntermediateRepresentation;
+using Ryujinx.Graphics.Shader.StructuredIr;
using Ryujinx.Graphics.Shader.Translation;
using System.Collections.Generic;
using System.Text;
@@ -75,6 +77,21 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
AppendLine("}" + suffix);
}
+ public int FindTextureDescriptorIndex(AstTextureOperation texOp)
+ {
+ AstOperand operand = texOp.GetSource(0) as AstOperand;
+ bool bindless = (texOp.Flags & TextureFlags.Bindless) > 0;
+
+ int cBufSlot = bindless ? operand.CbufSlot : 0;
+ int cBufOffset = bindless ? operand.CbufOffset : 0;
+
+ return TextureDescriptors.FindIndex(descriptor =>
+ descriptor.Type == texOp.Type &&
+ descriptor.HandleIndex == texOp.Handle &&
+ descriptor.CbufSlot == cBufSlot &&
+ descriptor.CbufOffset == cBufOffset);
+ }
+
private void UpdateIndentation()
{
_indentation = GetIndentation(_level);
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
index bd947ab71..f9d619286 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
@@ -137,6 +137,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
context.AppendLine();
}
+ if (context.Config.Stage == ShaderStage.Fragment || context.Config.Stage == ShaderStage.Compute)
+ {
+ if (DeclareRenderScale(context))
+ {
+ context.AppendLine();
+ }
+ }
+
if ((info.HelperFunctionsMask & HelperFunctionsMask.MultiplyHighS32) != 0)
{
AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/MultiplyHighS32.glsl");
@@ -219,6 +227,33 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
}
}
+ private static bool DeclareRenderScale(CodeGenContext context)
+ {
+ if ((context.Config.UsedFeatures & (FeatureFlags.FragCoordXY | FeatureFlags.IntegerSampling)) != 0)
+ {
+ string stage = OperandManager.GetShaderStagePrefix(context.Config.Stage);
+
+ int scaleElements = context.TextureDescriptors.Count;
+
+ if (context.Config.Stage == ShaderStage.Fragment)
+ {
+ scaleElements++; // Also includes render target scale, for gl_FragCoord.
+ }
+
+ context.AppendLine($"uniform float {stage}_renderScale[{scaleElements}];");
+
+ if (context.Config.UsedFeatures.HasFlag(FeatureFlags.IntegerSampling))
+ {
+ context.AppendLine();
+ AppendHelperFunction(context, $"Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_{stage}.glsl");
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
private static void DeclareStorages(CodeGenContext context, StructuredProgramInfo info)
{
string sbName = OperandManager.GetShaderStagePrefix(context.Config.Stage);
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_cp.glsl b/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_cp.glsl
new file mode 100644
index 000000000..381566d37
--- /dev/null
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_cp.glsl
@@ -0,0 +1,7 @@
+ivec2 Helper_TexelFetchScale(ivec2 inputVec, int samplerIndex) {
+ float scale = cp_renderScale[samplerIndex];
+ if (scale == 1.0) {
+ return inputVec;
+ }
+ return ivec2(vec2(inputVec) * scale);
+}
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_fp.glsl b/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_fp.glsl
new file mode 100644
index 000000000..4efaa65af
--- /dev/null
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_fp.glsl
@@ -0,0 +1,11 @@
+ivec2 Helper_TexelFetchScale(ivec2 inputVec, int samplerIndex) {
+ float scale = fp_renderScale[1 + samplerIndex];
+ if (scale == 1.0) {
+ return inputVec;
+ }
+ if (scale < 0.0) { // If less than 0, try interpolate between texels by using the screen position.
+ return ivec2(vec2(inputVec) * (-scale) + mod(gl_FragCoord.xy, -scale));
+ } else {
+ return ivec2(vec2(inputVec) * scale);
+ }
+}
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
index d05c77df6..b951798d5 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
@@ -390,7 +390,34 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
}
}
- Append(AssemblePVector(pCount));
+ string ApplyScaling(string vector)
+ {
+ if (intCoords)
+ {
+ int index = context.FindTextureDescriptorIndex(texOp);
+
+ if ((context.Config.Stage == ShaderStage.Fragment || context.Config.Stage == ShaderStage.Compute) &&
+ (texOp.Flags & TextureFlags.Bindless) == 0 &&
+ texOp.Type != SamplerType.Indexed &&
+ pCount == 2)
+ {
+ return "Helper_TexelFetchScale(" + vector + ", " + index + ")";
+ }
+ else
+ {
+ // Resolution scaling cannot be applied to this texture right now.
+ // Flag so that we know to blacklist scaling on related textures when binding them.
+
+ TextureDescriptor descriptor = context.TextureDescriptors[index];
+ descriptor.Flags |= TextureUsageFlags.ResScaleUnsupported;
+ context.TextureDescriptors[index] = descriptor;
+ }
+ }
+
+ return vector;
+ }
+
+ Append(ApplyScaling(AssemblePVector(pCount)));
string AssembleDerivativesVector(int count)
{
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
index 4c3f802a8..971284f4c 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
@@ -185,8 +185,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
{
switch (value & ~3)
{
- case AttributeConsts.PositionX: return "gl_FragCoord.x";
- case AttributeConsts.PositionY: return "gl_FragCoord.y";
+ case AttributeConsts.PositionX: return "(gl_FragCoord.x / fp_renderScale[0])";
+ case AttributeConsts.PositionY: return "(gl_FragCoord.y / fp_renderScale[0])";
case AttributeConsts.PositionZ: return "gl_FragCoord.z";
case AttributeConsts.PositionW: return "gl_FragCoord.w";
}
diff --git a/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs b/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs
index 2418293d4..24ba9a06d 100644
--- a/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs
+++ b/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs
@@ -32,6 +32,8 @@ namespace Ryujinx.Graphics.Shader.Instructions
Operand src = Attribute(op.AttributeOffset + index * 4);
+ context.FlagAttributeRead(src.Value);
+
context.Copy(Register(rd), context.LoadAttribute(src, primVertex));
}
}
@@ -96,6 +98,8 @@ namespace Ryujinx.Graphics.Shader.Instructions
{
OpCodeIpa op = (OpCodeIpa)context.CurrOp;
+ context.FlagAttributeRead(op.AttributeOffset);
+
Operand res = Attribute(op.AttributeOffset);
if (op.AttributeOffset >= AttributeConsts.UserAttributeBase &&
diff --git a/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs b/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
index 304906d0e..ea153b112 100644
--- a/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
+++ b/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
@@ -283,11 +283,15 @@ namespace Ryujinx.Graphics.Shader.Instructions
public static void Tld(EmitterContext context)
{
+ context.UsedFeatures |= FeatureFlags.IntegerSampling;
+
EmitTextureSample(context, TextureFlags.IntCoords);
}
public static void TldB(EmitterContext context)
{
+ context.UsedFeatures |= FeatureFlags.IntegerSampling;
+
EmitTextureSample(context, TextureFlags.IntCoords | TextureFlags.Bindless);
}
@@ -428,6 +432,8 @@ namespace Ryujinx.Graphics.Shader.Instructions
return;
}
+ context.UsedFeatures |= FeatureFlags.IntegerSampling;
+
flags = ConvertTextureFlags(tldsOp.Target) | TextureFlags.IntCoords;
if (tldsOp.Target == TexelLoadTarget.Texture1DLodZero && context.Config.GpuAccessor.QueryIsTextureBuffer(tldsOp.Immediate))
diff --git a/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj b/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
index d53bdce9a..b2d8a2a77 100644
--- a/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
+++ b/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
@@ -8,6 +8,8 @@
+
+
diff --git a/Ryujinx.Graphics.Shader/TextureDescriptor.cs b/Ryujinx.Graphics.Shader/TextureDescriptor.cs
index fae9b58c3..a9900fb89 100644
--- a/Ryujinx.Graphics.Shader/TextureDescriptor.cs
+++ b/Ryujinx.Graphics.Shader/TextureDescriptor.cs
@@ -13,6 +13,8 @@ namespace Ryujinx.Graphics.Shader
public int CbufSlot { get; }
public int CbufOffset { get; }
+ public TextureUsageFlags Flags { get; set; }
+
public TextureDescriptor(string name, SamplerType type, int handleIndex)
{
Name = name;
@@ -23,6 +25,8 @@ namespace Ryujinx.Graphics.Shader
CbufSlot = 0;
CbufOffset = 0;
+
+ Flags = TextureUsageFlags.None;
}
public TextureDescriptor(string name, SamplerType type, int cbufSlot, int cbufOffset)
@@ -35,6 +39,8 @@ namespace Ryujinx.Graphics.Shader
CbufSlot = cbufSlot;
CbufOffset = cbufOffset;
+
+ Flags = TextureUsageFlags.None;
}
}
}
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/TextureUsageFlags.cs b/Ryujinx.Graphics.Shader/TextureUsageFlags.cs
new file mode 100644
index 000000000..d9fa1a504
--- /dev/null
+++ b/Ryujinx.Graphics.Shader/TextureUsageFlags.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace Ryujinx.Graphics.Shader
+{
+ ///
+ /// Flags that indicate how a texture will be used in a shader.
+ ///
+ [Flags]
+ public enum TextureUsageFlags
+ {
+ None = 0,
+
+ // Integer sampled textures must be noted for resolution scaling.
+ ResScaleUnsupported = 1 << 0
+ }
+}
diff --git a/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs b/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
index 70476dcd1..39532a64f 100644
--- a/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
+++ b/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
@@ -11,6 +11,8 @@ namespace Ryujinx.Graphics.Shader.Translation
public Block CurrBlock { get; set; }
public OpCode CurrOp { get; set; }
+ public FeatureFlags UsedFeatures { get; set; }
+
public ShaderConfig Config { get; }
private List _operations;
@@ -40,6 +42,20 @@ namespace Ryujinx.Graphics.Shader.Translation
_operations.Add(operation);
}
+ public void FlagAttributeRead(int attribute)
+ {
+ if (Config.Stage == ShaderStage.Fragment)
+ {
+ switch (attribute)
+ {
+ case AttributeConsts.PositionX:
+ case AttributeConsts.PositionY:
+ UsedFeatures |= FeatureFlags.FragCoordXY;
+ break;
+ }
+ }
+ }
+
public void MarkLabel(Operand label)
{
Add(Instruction.MarkLabel, label);
diff --git a/Ryujinx.Graphics.Shader/Translation/FeatureFlags.cs b/Ryujinx.Graphics.Shader/Translation/FeatureFlags.cs
new file mode 100644
index 000000000..9c65038a9
--- /dev/null
+++ b/Ryujinx.Graphics.Shader/Translation/FeatureFlags.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace Ryujinx.Graphics.Shader.Translation
+{
+ ///
+ /// Features used by the shader that are important for the code generator to know in advance.
+ /// These typically change the declarations in the shader header.
+ ///
+ [Flags]
+ public enum FeatureFlags
+ {
+ None = 0,
+
+ // Affected by resolution scaling.
+ FragCoordXY = 1 << 1,
+ IntegerSampling = 1 << 0
+ }
+}
diff --git a/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs b/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs
index 8b38afb9a..aabd9ca66 100644
--- a/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs
+++ b/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs
@@ -22,6 +22,8 @@ namespace Ryujinx.Graphics.Shader.Translation
public TranslationFlags Flags { get; }
+ public FeatureFlags UsedFeatures { get; set; }
+
public ShaderConfig(IGpuAccessor gpuAccessor, TranslationFlags flags)
{
Stage = ShaderStage.Compute;
@@ -34,6 +36,7 @@ namespace Ryujinx.Graphics.Shader.Translation
OmapDepth = false;
GpuAccessor = gpuAccessor;
Flags = flags;
+ UsedFeatures = FeatureFlags.None;
}
public ShaderConfig(ShaderHeader header, IGpuAccessor gpuAccessor, TranslationFlags flags)
@@ -48,6 +51,7 @@ namespace Ryujinx.Graphics.Shader.Translation
OmapDepth = header.OmapDepth;
GpuAccessor = gpuAccessor;
Flags = flags;
+ UsedFeatures = FeatureFlags.None;
}
public int GetDepthRegister()
diff --git a/Ryujinx.Graphics.Shader/Translation/Translator.cs b/Ryujinx.Graphics.Shader/Translation/Translator.cs
index 1a69c5114..1d7aacdd2 100644
--- a/Ryujinx.Graphics.Shader/Translation/Translator.cs
+++ b/Ryujinx.Graphics.Shader/Translation/Translator.cs
@@ -16,15 +16,19 @@ namespace Ryujinx.Graphics.Shader.Translation
public static ShaderProgram Translate(ulong address, IGpuAccessor gpuAccessor, TranslationFlags flags)
{
- Operation[] ops = DecodeShader(address, gpuAccessor, flags, out ShaderConfig config, out int size);
+ Operation[] ops = DecodeShader(address, gpuAccessor, flags, out ShaderConfig config, out int size, out FeatureFlags featureFlags);
+
+ config.UsedFeatures = featureFlags;
return Translate(ops, config, size);
}
public static ShaderProgram Translate(ulong addressA, ulong addressB, IGpuAccessor gpuAccessor, TranslationFlags flags)
{
- Operation[] opsA = DecodeShader(addressA, gpuAccessor, flags | TranslationFlags.VertexA, out _, out int sizeA);
- Operation[] opsB = DecodeShader(addressB, gpuAccessor, flags, out ShaderConfig config, out int sizeB);
+ Operation[] opsA = DecodeShader(addressA, gpuAccessor, flags | TranslationFlags.VertexA, out _, out int sizeA, out FeatureFlags featureFlagsA);
+ Operation[] opsB = DecodeShader(addressB, gpuAccessor, flags, out ShaderConfig config, out int sizeB, out FeatureFlags featureFlagsB);
+
+ config.UsedFeatures = featureFlagsA | featureFlagsB;
return Translate(Combine(opsA, opsB), config, sizeB, sizeA);
}
@@ -67,7 +71,8 @@ namespace Ryujinx.Graphics.Shader.Translation
IGpuAccessor gpuAccessor,
TranslationFlags flags,
out ShaderConfig config,
- out int size)
+ out int size,
+ out FeatureFlags featureFlags)
{
Block[] cfg;
@@ -90,6 +95,8 @@ namespace Ryujinx.Graphics.Shader.Translation
size = 0;
+ featureFlags = FeatureFlags.None;
+
return Array.Empty();
}
@@ -192,6 +199,8 @@ namespace Ryujinx.Graphics.Shader.Translation
size = (int)maxEndAddress + (((flags & TranslationFlags.Compute) != 0) ? 0 : HeaderSize);
+ featureFlags = context.UsedFeatures;
+
return context.GetOperations();
}
diff --git a/Ryujinx/Config.json b/Ryujinx/Config.json
index 4551b869f..2ca59df84 100644
--- a/Ryujinx/Config.json
+++ b/Ryujinx/Config.json
@@ -1,5 +1,7 @@
{
- "version": 10,
+ "version": 11,
+ "res_scale": 2,
+ "res_scale_custom": 1,
"max_anisotropy": -1,
"graphics_shaders_dump_path": "",
"logging_enable_debug": false,
diff --git a/Ryujinx/Ui/GLRenderer.cs b/Ryujinx/Ui/GLRenderer.cs
index 17b592a70..a8ed9156c 100644
--- a/Ryujinx/Ui/GLRenderer.cs
+++ b/Ryujinx/Ui/GLRenderer.cs
@@ -328,6 +328,11 @@ namespace Ryujinx.Ui
}
string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? "Docked" : "Handheld";
+ float scale = Graphics.Gpu.GraphicsConfig.ResScale;
+ if (scale != 1)
+ {
+ dockedMode += $" ({scale}x)";
+ }
if (_ticks >= _ticksPerFrame)
{
diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs
index 0e3262b2d..27fcd3339 100644
--- a/Ryujinx/Ui/MainWindow.cs
+++ b/Ryujinx/Ui/MainWindow.cs
@@ -390,9 +390,7 @@ namespace Ryujinx.Ui
HLE.Switch device = InitializeSwitchInstance();
- // TODO: Move this somewhere else + reloadable?
- Graphics.Gpu.GraphicsConfig.MaxAnisotropy = ConfigurationState.Instance.Graphics.MaxAnisotropy;
- Graphics.Gpu.GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath;
+ UpdateGraphicsConfig();
Logger.PrintInfo(LogClass.Application, $"Using Firmware Version: {_contentManager.GetCurrentFirmwareVersion()?.VersionString}");
@@ -605,6 +603,15 @@ namespace Ryujinx.Ui
}
}
+ public static void UpdateGraphicsConfig()
+ {
+ int resScale = ConfigurationState.Instance.Graphics.ResScale;
+ float resScaleCustom = ConfigurationState.Instance.Graphics.ResScaleCustom;
+ Graphics.Gpu.GraphicsConfig.ResScale = (resScale == -1) ? resScaleCustom : resScale;
+ Graphics.Gpu.GraphicsConfig.MaxAnisotropy = ConfigurationState.Instance.Graphics.MaxAnisotropy;
+ Graphics.Gpu.GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath;
+ }
+
public static void SaveConfig()
{
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
diff --git a/Ryujinx/Ui/SettingsWindow.cs b/Ryujinx/Ui/SettingsWindow.cs
index e6d5c8df8..a278f0949 100644
--- a/Ryujinx/Ui/SettingsWindow.cs
+++ b/Ryujinx/Ui/SettingsWindow.cs
@@ -63,6 +63,8 @@ namespace Ryujinx.Ui
[GUI] Entry _addGameDirBox;
[GUI] Entry _graphicsShadersDumpPath;
[GUI] ComboBoxText _anisotropy;
+ [GUI] ComboBoxText _resScaleCombo;
+ [GUI] Entry _resScaleText;
[GUI] ToggleButton _configureController1;
[GUI] ToggleButton _configureController2;
[GUI] ToggleButton _configureController3;
@@ -95,6 +97,8 @@ namespace Ryujinx.Ui
_configureController8.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Player8);
_configureControllerH.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Handheld);
+ _resScaleCombo.Changed += (sender, args) => _resScaleText.Visible = _resScaleCombo.ActiveId == "-1";
+
//Setup Currents
if (ConfigurationState.Instance.Logger.EnableFileLog)
{
@@ -204,9 +208,12 @@ namespace Ryujinx.Ui
_systemRegionSelect.SetActiveId(ConfigurationState.Instance.System.Region.Value.ToString());
_audioBackendSelect.SetActiveId(ConfigurationState.Instance.System.AudioBackend.Value.ToString());
_systemTimeZoneSelect.SetActiveId(timeZoneContentManager.SanityCheckDeviceLocationName());
+ _resScaleCombo.SetActiveId(ConfigurationState.Instance.Graphics.ResScale.Value.ToString());
_anisotropy.SetActiveId(ConfigurationState.Instance.Graphics.MaxAnisotropy.Value.ToString());
_custThemePath.Buffer.Text = ConfigurationState.Instance.Ui.CustomThemePath;
+ _resScaleText.Buffer.Text = ConfigurationState.Instance.Graphics.ResScaleCustom.Value.ToString();
+ _resScaleText.Visible = _resScaleCombo.ActiveId == "-1";
_graphicsShadersDumpPath.Buffer.Text = ConfigurationState.Instance.Graphics.ShadersDumpPath;
_fsLogSpinAdjustment.Value = ConfigurationState.Instance.System.FsGlobalAccessLogMode;
_systemTimeOffset = ConfigurationState.Instance.System.SystemTimeOffset;
@@ -408,6 +415,12 @@ namespace Ryujinx.Ui
_gameDirsBoxStore.IterNext(ref treeIter);
}
+ float resScaleCustom;
+ if (!float.TryParse(_resScaleText.Buffer.Text, out resScaleCustom) || resScaleCustom <= 0.0f)
+ {
+ resScaleCustom = 1.0f;
+ }
+
ConfigurationState.Instance.Logger.EnableError.Value = _errorLogToggle.Active;
ConfigurationState.Instance.Logger.EnableWarn.Value = _warningLogToggle.Active;
ConfigurationState.Instance.Logger.EnableInfo.Value = _infoLogToggle.Active;
@@ -435,8 +448,11 @@ namespace Ryujinx.Ui
ConfigurationState.Instance.Ui.GameDirs.Value = gameDirs;
ConfigurationState.Instance.System.FsGlobalAccessLogMode.Value = (int)_fsLogSpinAdjustment.Value;
ConfigurationState.Instance.Graphics.MaxAnisotropy.Value = float.Parse(_anisotropy.ActiveId);
+ ConfigurationState.Instance.Graphics.ResScale.Value = int.Parse(_resScaleCombo.ActiveId);
+ ConfigurationState.Instance.Graphics.ResScaleCustom.Value = resScaleCustom;
MainWindow.SaveConfig();
+ MainWindow.UpdateGraphicsConfig();
MainWindow.ApplyTheme();
Dispose();
}
diff --git a/Ryujinx/Ui/SettingsWindow.glade b/Ryujinx/Ui/SettingsWindow.glade
index 52171031d..1e91011ec 100644
--- a/Ryujinx/Ui/SettingsWindow.glade
+++ b/Ryujinx/Ui/SettingsWindow.glade
@@ -1677,6 +1677,70 @@
10
10
vertical
+
+
+
+ False
+ True
+ 5
+ 0
+
+
True
@@ -1722,7 +1786,7 @@
False
True
5
- 0
+ 1
diff --git a/Ryujinx/_schema.json b/Ryujinx/_schema.json
index e53b49cb0..90b993e69 100644
--- a/Ryujinx/_schema.json
+++ b/Ryujinx/_schema.json
@@ -700,6 +700,27 @@
}
},
"properties": {
+ "res_scale": {
+ "$id": "#/properties/res_scale",
+ "type": "integer",
+ "title": "Resolution Scale",
+ "description": "An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead.",
+ "default": -1,
+ "examples": [
+ -1,
+ 1,
+ 2,
+ 3,
+ 4
+ ]
+ },
+ "res_scale_custom": {
+ "$id": "#/properties/res_scale_custom",
+ "type": "number",
+ "title": "Custom Resolution Scale",
+ "description": "A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1.",
+ "default": 1.0,
+ },
"max_anisotropy": {
"$id": "#/properties/max_anisotropy",
"type": "integer",
@@ -1211,7 +1232,7 @@
"button_sr": "Unbound"
}
}
- ]
+ ]
},
"controller_config": {
"$id": "#/properties/controller_config",