From 484eb645ae0611f60fae845ed011ed6115352e06 Mon Sep 17 00:00:00 2001 From: riperiperi Date: Tue, 7 Jul 2020 03:41:07 +0100 Subject: [PATCH] Implement Zero-Configuration Resolution Scaling (#1365) * Initial implementation of Render Target Scaling Works with most games I have. No GUI option right now, it is hardcoded. Missing handling for texelFetch operation. * Realtime Configuration, refactoring. * texelFetch scaling on fragment shader (WIP) * Improve Shader-Side changes. * Fix potential crash when no color/depth bound * Workaround random uses of textures in compute. This was blacklisting textures in a few games despite causing no bugs. Will eventually add full support so this doesn't break anything. * Fix scales oscillating when changing between non-native scales. * Scaled textures on compute, cleanup, lazier uniform update. * Cleanup. * Fix stupidity * Address Thog Feedback. * Cover most of GDK's feedback (two comments remain) * Fix bad rename * Move IsDepthStencil to FormatExtensions, add docs. * Fix default config, square texture detection. * Three final fixes: - Nearest copy when texture is integer format. - Texture2D -> Texture3D copy correctly blacklists the texture before trying an unscaled copy (caused driver error) - Discount small textures. * Remove scale threshold. Not needed right now - we'll see if we run into problems. * All CPU modification blacklists scale. * Fix comment. --- .../Configuration/ConfigurationFileFormat.cs | 12 +- .../Configuration/ConfigurationState.cs | 28 +++ Ryujinx.Graphics.GAL/Format.cs | 141 ++++++++++++ Ryujinx.Graphics.GAL/IPipeline.cs | 4 + Ryujinx.Graphics.GAL/IRenderer.cs | 2 +- Ryujinx.Graphics.GAL/ITexture.cs | 4 + Ryujinx.Graphics.Gpu/Engine/Compute.cs | 6 +- Ryujinx.Graphics.Gpu/Engine/MethodClear.cs | 6 +- .../Engine/MethodCopyTexture.cs | 42 ++-- Ryujinx.Graphics.Gpu/Engine/Methods.cs | 51 +++- Ryujinx.Graphics.Gpu/GraphicsConfig.cs | 5 + Ryujinx.Graphics.Gpu/Image/Texture.cs | 217 ++++++++++++++++-- .../Image/TextureBindingInfo.cs | 16 +- .../Image/TextureBindingsManager.cs | 19 ++ Ryujinx.Graphics.Gpu/Image/TextureManager.cs | 187 ++++++++++++++- Ryujinx.Graphics.Gpu/Image/TexturePool.cs | 22 +- .../Image/TextureScaleMode.cs | 14 ++ .../Image/TextureSearchFlags.cs | 3 +- Ryujinx.Graphics.Gpu/Window.cs | 2 +- Ryujinx.Graphics.OpenGL/Image/TextureBase.cs | 11 +- Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs | 8 + .../Image/TextureCopyUnscaled.cs | 11 +- .../Image/TextureStorage.cs | 42 ++-- Ryujinx.Graphics.OpenGL/Image/TextureView.cs | 15 +- Ryujinx.Graphics.OpenGL/Pipeline.cs | 84 +++++++ Ryujinx.Graphics.OpenGL/Program.cs | 6 + Ryujinx.Graphics.OpenGL/Renderer.cs | 4 +- Ryujinx.Graphics.OpenGL/Window.cs | 13 +- .../CodeGen/Glsl/CodeGenContext.cs | 17 ++ .../CodeGen/Glsl/Declarations.cs | 35 +++ .../HelperFunctions/TexelFetchScale_cp.glsl | 7 + .../HelperFunctions/TexelFetchScale_fp.glsl | 11 + .../Glsl/Instructions/InstGenMemory.cs | 29 ++- .../CodeGen/Glsl/OperandManager.cs | 4 +- .../Instructions/InstEmitMemory.cs | 4 + .../Instructions/InstEmitTexture.cs | 6 + .../Ryujinx.Graphics.Shader.csproj | 2 + Ryujinx.Graphics.Shader/TextureDescriptor.cs | 6 + Ryujinx.Graphics.Shader/TextureUsageFlags.cs | 16 ++ .../Translation/EmitterContext.cs | 16 ++ .../Translation/FeatureFlags.cs | 18 ++ .../Translation/ShaderConfig.cs | 4 + .../Translation/Translator.cs | 17 +- Ryujinx/Config.json | 4 +- Ryujinx/Ui/GLRenderer.cs | 5 + Ryujinx/Ui/MainWindow.cs | 13 +- Ryujinx/Ui/SettingsWindow.cs | 16 ++ Ryujinx/Ui/SettingsWindow.glade | 66 +++++- Ryujinx/_schema.json | 23 +- 49 files changed, 1163 insertions(+), 131 deletions(-) create mode 100644 Ryujinx.Graphics.Gpu/Image/TextureScaleMode.cs create mode 100644 Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_cp.glsl create mode 100644 Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_fp.glsl create mode 100644 Ryujinx.Graphics.Shader/TextureUsageFlags.cs create mode 100644 Ryujinx.Graphics.Shader/Translation/FeatureFlags.cs 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 + + + True + False + 5 + 5 + + + True + False + Resolution Scale applied to applicable render targets. + Resolution Scale: + + + False + True + 5 + 0 + + + + + True + False + Resolution Scale applied to applicable render targets. + 1 + + Native (720p/1080p) + 2x (1440p/2160p) + 3x (2160p/3240p) + 4x (2880p/4320p) + Custom (not recommended) + + + + False + True + 1 + + + + + True + True + Floating point resolution scale, such as 1.5. Non-integral scales are more likely to cause issues or crash. + center + False + 1.0 + GTK_INPUT_PURPOSE_NUMBER + + + True + True + 2 + + + + + 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",