Compare commits

..

4 Commits

Author SHA1 Message Date
2b6cc4b353 Add the "Auto" theme option in setting (#6611)
* Add "Follow OS theme" option

* Update App.axaml.cs

* Add "Follow OS theme" option

* Update App.axaml.cs

* Remove `this`

* Remove annotation for nullable reference

* Change into switch expression to make it concise

* Change comments to XML docs

* Update en_US.json

* Fix icons in About dialog do not response to "auto" theme

The theme icons seemingly use Dark variant event when the OS theme is light. In addition, I added ThemeManager common to make it accessible for both App and AboutWindow

* Newline at the end

* newline moment

* Update ThemeManager.cs

* bait to switch to lf

* change to lf

* temp. revert

* Add back ThemeManager.cs common, pls pass the format check

* I found the mistake: should have put `ThemeManager.OnThemeChanged();` in try block

Finally solve the formatting check

* test formatting

* Update App.axaml.cs

* Ok i seem to forget to add version lol

* Fix info CA1816
2024-05-14 17:00:03 +02:00
075575200d Update compatibility information in README.md (#6801) 2024-05-14 16:58:48 +02:00
3a3b51893e Add support for bindless textures from storage buffer on Vulkan (#6721)
* Halve primitive ID when converting quads to triangles

* Shader cache version bump

* Add support for bindless textures from storage buffer on Vulkan
2024-05-14 16:47:16 +02:00
44dbab3848 discordRPC: Truncate game title and details if they exceed discord byte limit. (#6581)
* Truncate game title and details if they exceed DiscordRPC limit.

* Update implementation to a byte total check.

* Track if the string has actually been modified correctly.

* Allow an early function return and simplify logic.

* Better handling of large input strings and minimise loop opportunities.

* Remove unused using.

* Update to `applicationName` over `titleName`.
2024-05-14 16:36:44 +02:00
23 changed files with 235 additions and 43 deletions

View File

@ -36,8 +36,8 @@
## Compatibility ## Compatibility
As of October 2023, Ryujinx has been tested on approximately 4,200 titles; As of May 2024, Ryujinx has been tested on approximately 4,300 titles;
over 4,150 boot past menus and into gameplay, with roughly 3,500 of those being considered playable. over 4,100 boot past menus and into gameplay, with roughly 3,550 of those being considered playable.
You can check out the compatibility list [here](https://github.com/Ryujinx/Ryujinx-Games-List/issues). You can check out the compatibility list [here](https://github.com/Ryujinx/Ryujinx-Games-List/issues).

View File

@ -36,6 +36,7 @@ namespace Ryujinx.Graphics.GAL
public readonly bool SupportsMismatchingViewFormat; public readonly bool SupportsMismatchingViewFormat;
public readonly bool SupportsCubemapView; public readonly bool SupportsCubemapView;
public readonly bool SupportsNonConstantTextureOffset; public readonly bool SupportsNonConstantTextureOffset;
public readonly bool SupportsQuads;
public readonly bool SupportsSeparateSampler; public readonly bool SupportsSeparateSampler;
public readonly bool SupportsShaderBallot; public readonly bool SupportsShaderBallot;
public readonly bool SupportsShaderBarrierDivergence; public readonly bool SupportsShaderBarrierDivergence;
@ -93,6 +94,7 @@ namespace Ryujinx.Graphics.GAL
bool supportsMismatchingViewFormat, bool supportsMismatchingViewFormat,
bool supportsCubemapView, bool supportsCubemapView,
bool supportsNonConstantTextureOffset, bool supportsNonConstantTextureOffset,
bool supportsQuads,
bool supportsSeparateSampler, bool supportsSeparateSampler,
bool supportsShaderBallot, bool supportsShaderBallot,
bool supportsShaderBarrierDivergence, bool supportsShaderBarrierDivergence,
@ -146,6 +148,7 @@ namespace Ryujinx.Graphics.GAL
SupportsMismatchingViewFormat = supportsMismatchingViewFormat; SupportsMismatchingViewFormat = supportsMismatchingViewFormat;
SupportsCubemapView = supportsCubemapView; SupportsCubemapView = supportsCubemapView;
SupportsNonConstantTextureOffset = supportsNonConstantTextureOffset; SupportsNonConstantTextureOffset = supportsNonConstantTextureOffset;
SupportsQuads = supportsQuads;
SupportsSeparateSampler = supportsSeparateSampler; SupportsSeparateSampler = supportsSeparateSampler;
SupportsShaderBallot = supportsShaderBallot; SupportsShaderBallot = supportsShaderBallot;
SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence; SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence;

View File

@ -18,6 +18,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
private readonly ShaderSpecializationState _newSpecState; private readonly ShaderSpecializationState _newSpecState;
private readonly int _stageIndex; private readonly int _stageIndex;
private readonly bool _isVulkan; private readonly bool _isVulkan;
private readonly bool _hasGeometryShader;
private readonly bool _supportsQuads;
/// <summary> /// <summary>
/// Creates a new instance of the cached GPU state accessor for shader translation. /// Creates a new instance of the cached GPU state accessor for shader translation.
@ -29,6 +31,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
/// <param name="newSpecState">Shader specialization state of the recompiled shader</param> /// <param name="newSpecState">Shader specialization state of the recompiled shader</param>
/// <param name="counts">Resource counts shared across all shader stages</param> /// <param name="counts">Resource counts shared across all shader stages</param>
/// <param name="stageIndex">Shader stage index</param> /// <param name="stageIndex">Shader stage index</param>
/// <param name="hasGeometryShader">Indicates if a geometry shader is present</param>
public DiskCacheGpuAccessor( public DiskCacheGpuAccessor(
GpuContext context, GpuContext context,
ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> data,
@ -36,7 +39,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
ShaderSpecializationState oldSpecState, ShaderSpecializationState oldSpecState,
ShaderSpecializationState newSpecState, ShaderSpecializationState newSpecState,
ResourceCounts counts, ResourceCounts counts,
int stageIndex) : base(context, counts, stageIndex) int stageIndex,
bool hasGeometryShader) : base(context, counts, stageIndex)
{ {
_data = data; _data = data;
_cb1Data = cb1Data; _cb1Data = cb1Data;
@ -44,6 +48,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
_newSpecState = newSpecState; _newSpecState = newSpecState;
_stageIndex = stageIndex; _stageIndex = stageIndex;
_isVulkan = context.Capabilities.Api == TargetApi.Vulkan; _isVulkan = context.Capabilities.Api == TargetApi.Vulkan;
_hasGeometryShader = hasGeometryShader;
_supportsQuads = context.Capabilities.SupportsQuads;
if (stageIndex == (int)ShaderStage.Geometry - 1) if (stageIndex == (int)ShaderStage.Geometry - 1)
{ {
@ -100,7 +106,11 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
/// <inheritdoc/> /// <inheritdoc/>
public GpuGraphicsState QueryGraphicsState() public GpuGraphicsState QueryGraphicsState()
{ {
return _oldSpecState.GraphicsState.CreateShaderGraphicsState(!_isVulkan, _isVulkan || _oldSpecState.GraphicsState.YNegateEnabled); return _oldSpecState.GraphicsState.CreateShaderGraphicsState(
!_isVulkan,
_supportsQuads,
_hasGeometryShader,
_isVulkan || _oldSpecState.GraphicsState.YNegateEnabled);
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
private const ushort FileFormatVersionMajor = 1; private const ushort FileFormatVersionMajor = 1;
private const ushort FileFormatVersionMinor = 2; private const ushort FileFormatVersionMinor = 2;
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor; private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
private const uint CodeGenVersion = 6577; private const uint CodeGenVersion = 5936;
private const string SharedTocFileName = "shared.toc"; private const string SharedTocFileName = "shared.toc";
private const string SharedDataFileName = "shared.data"; private const string SharedDataFileName = "shared.data";

View File

@ -601,6 +601,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
TargetApi api = _context.Capabilities.Api; TargetApi api = _context.Capabilities.Api;
bool hasCachedGs = guestShaders[4].HasValue;
for (int stageIndex = Constants.ShaderStages - 1; stageIndex >= 0; stageIndex--) for (int stageIndex = Constants.ShaderStages - 1; stageIndex >= 0; stageIndex--)
{ {
if (guestShaders[stageIndex + 1].HasValue) if (guestShaders[stageIndex + 1].HasValue)
@ -610,7 +612,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
byte[] guestCode = shader.Code; byte[] guestCode = shader.Code;
byte[] cb1Data = shader.Cb1Data; byte[] cb1Data = shader.Cb1Data;
DiskCacheGpuAccessor gpuAccessor = new(_context, guestCode, cb1Data, specState, newSpecState, counts, stageIndex); DiskCacheGpuAccessor gpuAccessor = new(_context, guestCode, cb1Data, specState, newSpecState, counts, stageIndex, hasCachedGs);
TranslatorContext currentStage = DecodeGraphicsShader(gpuAccessor, api, DefaultFlags, 0); TranslatorContext currentStage = DecodeGraphicsShader(gpuAccessor, api, DefaultFlags, 0);
if (nextStage != null) if (nextStage != null)
@ -623,7 +625,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
byte[] guestCodeA = guestShaders[0].Value.Code; byte[] guestCodeA = guestShaders[0].Value.Code;
byte[] cb1DataA = guestShaders[0].Value.Cb1Data; byte[] cb1DataA = guestShaders[0].Value.Cb1Data;
DiskCacheGpuAccessor gpuAccessorA = new(_context, guestCodeA, cb1DataA, specState, newSpecState, counts, 0); DiskCacheGpuAccessor gpuAccessorA = new(_context, guestCodeA, cb1DataA, specState, newSpecState, counts, 0, hasCachedGs);
translatorContexts[0] = DecodeGraphicsShader(gpuAccessorA, api, DefaultFlags | TranslationFlags.VertexA, 0); translatorContexts[0] = DecodeGraphicsShader(gpuAccessorA, api, DefaultFlags | TranslationFlags.VertexA, 0);
} }
@ -711,7 +713,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
GuestCodeAndCbData shader = guestShaders[0].Value; GuestCodeAndCbData shader = guestShaders[0].Value;
ResourceCounts counts = new(); ResourceCounts counts = new();
ShaderSpecializationState newSpecState = new(ref specState.ComputeState); ShaderSpecializationState newSpecState = new(ref specState.ComputeState);
DiskCacheGpuAccessor gpuAccessor = new(_context, shader.Code, shader.Cb1Data, specState, newSpecState, counts, 0); DiskCacheGpuAccessor gpuAccessor = new(_context, shader.Code, shader.Cb1Data, specState, newSpecState, counts, 0, false);
gpuAccessor.InitializeReservedCounts(tfEnabled: false, vertexAsCompute: false); gpuAccessor.InitializeReservedCounts(tfEnabled: false, vertexAsCompute: false);
TranslatorContext translatorContext = DecodeComputeShader(gpuAccessor, _context.Capabilities.Api, 0); TranslatorContext translatorContext = DecodeComputeShader(gpuAccessor, _context.Capabilities.Api, 0);

View File

@ -17,6 +17,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
private readonly int _stageIndex; private readonly int _stageIndex;
private readonly bool _compute; private readonly bool _compute;
private readonly bool _isVulkan; private readonly bool _isVulkan;
private readonly bool _hasGeometryShader;
private readonly bool _supportsQuads;
/// <summary> /// <summary>
/// Creates a new instance of the GPU state accessor for graphics shader translation. /// Creates a new instance of the GPU state accessor for graphics shader translation.
@ -25,12 +27,20 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// <param name="channel">GPU channel</param> /// <param name="channel">GPU channel</param>
/// <param name="state">Current GPU state</param> /// <param name="state">Current GPU state</param>
/// <param name="stageIndex">Graphics shader stage index (0 = Vertex, 4 = Fragment)</param> /// <param name="stageIndex">Graphics shader stage index (0 = Vertex, 4 = Fragment)</param>
public GpuAccessor(GpuContext context, GpuChannel channel, GpuAccessorState state, int stageIndex) : base(context, state.ResourceCounts, stageIndex) /// <param name="hasGeometryShader">Indicates if a geometry shader is present</param>
public GpuAccessor(
GpuContext context,
GpuChannel channel,
GpuAccessorState state,
int stageIndex,
bool hasGeometryShader) : base(context, state.ResourceCounts, stageIndex)
{ {
_isVulkan = context.Capabilities.Api == TargetApi.Vulkan;
_channel = channel; _channel = channel;
_state = state; _state = state;
_stageIndex = stageIndex; _stageIndex = stageIndex;
_isVulkan = context.Capabilities.Api == TargetApi.Vulkan;
_hasGeometryShader = hasGeometryShader;
_supportsQuads = context.Capabilities.SupportsQuads;
if (stageIndex == (int)ShaderStage.Geometry - 1) if (stageIndex == (int)ShaderStage.Geometry - 1)
{ {
@ -105,7 +115,11 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// <inheritdoc/> /// <inheritdoc/>
public GpuGraphicsState QueryGraphicsState() public GpuGraphicsState QueryGraphicsState()
{ {
return _state.GraphicsState.CreateShaderGraphicsState(!_isVulkan, _isVulkan || _state.GraphicsState.YNegateEnabled); return _state.GraphicsState.CreateShaderGraphicsState(
!_isVulkan,
_supportsQuads,
_hasGeometryShader,
_isVulkan || _state.GraphicsState.YNegateEnabled);
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@ -106,8 +106,11 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// Creates a new graphics state from this state that can be used for shader generation. /// Creates a new graphics state from this state that can be used for shader generation.
/// </summary> /// </summary>
/// <param name="hostSupportsAlphaTest">Indicates if the host API supports alpha test operations</param> /// <param name="hostSupportsAlphaTest">Indicates if the host API supports alpha test operations</param>
/// <param name="hostSupportsQuads">Indicates if the host API supports quad primitives</param>
/// <param name="hasGeometryShader">Indicates if a geometry shader is used</param>
/// <param name="originUpperLeft">If true, indicates that the fragment origin is the upper left corner of the viewport, otherwise it is the lower left corner</param>
/// <returns>GPU graphics state that can be used for shader translation</returns> /// <returns>GPU graphics state that can be used for shader translation</returns>
public readonly GpuGraphicsState CreateShaderGraphicsState(bool hostSupportsAlphaTest, bool originUpperLeft) public readonly GpuGraphicsState CreateShaderGraphicsState(bool hostSupportsAlphaTest, bool hostSupportsQuads, bool hasGeometryShader, bool originUpperLeft)
{ {
AlphaTestOp alphaTestOp; AlphaTestOp alphaTestOp;
@ -130,6 +133,9 @@ namespace Ryujinx.Graphics.Gpu.Shader
}; };
} }
bool isQuad = Topology == PrimitiveTopology.Quads || Topology == PrimitiveTopology.QuadStrip;
bool halvePrimitiveId = !hostSupportsQuads && !hasGeometryShader && isQuad;
return new GpuGraphicsState( return new GpuGraphicsState(
EarlyZForce, EarlyZForce,
ConvertToInputTopology(Topology, TessellationMode), ConvertToInputTopology(Topology, TessellationMode),
@ -149,7 +155,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
in FragmentOutputTypes, in FragmentOutputTypes,
DualSourceBlendEnable, DualSourceBlendEnable,
YNegateEnabled, YNegateEnabled,
originUpperLeft); originUpperLeft,
halvePrimitiveId);
} }
/// <summary> /// <summary>

View File

@ -339,7 +339,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
if (gpuVa != 0) if (gpuVa != 0)
{ {
GpuAccessor gpuAccessor = new(_context, channel, gpuAccessorState, stageIndex); GpuAccessor gpuAccessor = new(_context, channel, gpuAccessorState, stageIndex, addresses.Geometry != 0);
TranslatorContext currentStage = DecodeGraphicsShader(gpuAccessor, api, DefaultFlags, gpuVa); TranslatorContext currentStage = DecodeGraphicsShader(gpuAccessor, api, DefaultFlags, gpuVa);
if (nextStage != null) if (nextStage != null)

View File

@ -161,6 +161,7 @@ namespace Ryujinx.Graphics.OpenGL
supportsBgraFormat: false, supportsBgraFormat: false,
supportsR4G4Format: false, supportsR4G4Format: false,
supportsR4G4B4A4Format: true, supportsR4G4B4A4Format: true,
supportsScaledVertexFormats: true,
supportsSnormBufferTextureFormat: false, supportsSnormBufferTextureFormat: false,
supports5BitComponentFormat: true, supports5BitComponentFormat: true,
supportsSparseBuffer: false, supportsSparseBuffer: false,
@ -175,7 +176,7 @@ namespace Ryujinx.Graphics.OpenGL
supportsMismatchingViewFormat: HwCapabilities.SupportsMismatchingViewFormat, supportsMismatchingViewFormat: HwCapabilities.SupportsMismatchingViewFormat,
supportsCubemapView: true, supportsCubemapView: true,
supportsNonConstantTextureOffset: HwCapabilities.SupportsNonConstantTextureOffset, supportsNonConstantTextureOffset: HwCapabilities.SupportsNonConstantTextureOffset,
supportsScaledVertexFormats: true, supportsQuads: HwCapabilities.SupportsQuads,
supportsSeparateSampler: false, supportsSeparateSampler: false,
supportsShaderBallot: HwCapabilities.SupportsShaderBallot, supportsShaderBallot: HwCapabilities.SupportsShaderBallot,
supportsShaderBarrierDivergence: !(intelWindows || intelUnix), supportsShaderBarrierDivergence: !(intelWindows || intelUnix),

View File

@ -102,6 +102,11 @@ namespace Ryujinx.Graphics.Shader
/// </summary> /// </summary>
public readonly bool OriginUpperLeft; public readonly bool OriginUpperLeft;
/// <summary>
/// Indicates that the primitive ID values on the shader should be halved due to quad to triangles conversion.
/// </summary>
public readonly bool HalvePrimitiveId;
/// <summary> /// <summary>
/// Creates a new GPU graphics state. /// Creates a new GPU graphics state.
/// </summary> /// </summary>
@ -124,6 +129,7 @@ namespace Ryujinx.Graphics.Shader
/// <param name="dualSourceBlendEnable">Indicates whether dual source blend is enabled</param> /// <param name="dualSourceBlendEnable">Indicates whether dual source blend is enabled</param>
/// <param name="yNegateEnabled">Indicates if negation of the viewport Y axis is enabled</param> /// <param name="yNegateEnabled">Indicates if negation of the viewport Y axis is enabled</param>
/// <param name="originUpperLeft">If true, indicates that the fragment origin is the upper left corner of the viewport, otherwise it is the lower left corner</param> /// <param name="originUpperLeft">If true, indicates that the fragment origin is the upper left corner of the viewport, otherwise it is the lower left corner</param>
/// <param name="halvePrimitiveId">Indicates that the primitive ID values on the shader should be halved due to quad to triangles conversion</param>
public GpuGraphicsState( public GpuGraphicsState(
bool earlyZForce, bool earlyZForce,
InputTopology topology, InputTopology topology,
@ -143,7 +149,8 @@ namespace Ryujinx.Graphics.Shader
in Array8<AttributeType> fragmentOutputTypes, in Array8<AttributeType> fragmentOutputTypes,
bool dualSourceBlendEnable, bool dualSourceBlendEnable,
bool yNegateEnabled, bool yNegateEnabled,
bool originUpperLeft) bool originUpperLeft,
bool halvePrimitiveId)
{ {
EarlyZForce = earlyZForce; EarlyZForce = earlyZForce;
Topology = topology; Topology = topology;
@ -164,6 +171,7 @@ namespace Ryujinx.Graphics.Shader
DualSourceBlendEnable = dualSourceBlendEnable; DualSourceBlendEnable = dualSourceBlendEnable;
YNegateEnabled = yNegateEnabled; YNegateEnabled = yNegateEnabled;
OriginUpperLeft = originUpperLeft; OriginUpperLeft = originUpperLeft;
HalvePrimitiveId = halvePrimitiveId;
} }
} }
} }

View File

@ -135,6 +135,7 @@ namespace Ryujinx.Graphics.Shader
default, default,
false, false,
false, false,
false,
false); false);
} }

View File

@ -84,6 +84,10 @@ namespace Ryujinx.Graphics.Shader.Instructions
value = context.IConvertU32ToFP32(value); value = context.IConvertU32ToFP32(value);
} }
} }
else if (offset == AttributeConsts.PrimitiveId && context.TranslatorContext.Definitions.HalvePrimitiveId)
{
value = context.ShiftRightS32(value, Const(1));
}
context.Copy(Register(rd), value); context.Copy(Register(rd), value);
} }
@ -187,6 +191,12 @@ namespace Ryujinx.Graphics.Shader.Instructions
} }
} }
} }
else if (op.Imm10 == AttributeConsts.PrimitiveId && context.TranslatorContext.Definitions.HalvePrimitiveId)
{
// If quads are used, but the host does not support them, they need to be converted to triangles.
// Since each quad becomes 2 triangles, we need to compensate here and divide primitive ID by 2.
res = context.ShiftRightS32(res, Const(1));
}
else if (op.Imm10 == AttributeConsts.FrontFacing && context.TranslatorContext.GpuAccessor.QueryHostHasFrontFacingBug()) else if (op.Imm10 == AttributeConsts.FrontFacing && context.TranslatorContext.GpuAccessor.QueryHostHasFrontFacingBug())
{ {
// gl_FrontFacing sometimes has incorrect (flipped) values depending how it is accessed on Intel GPUs. // gl_FrontFacing sometimes has incorrect (flipped) values depending how it is accessed on Intel GPUs.

View File

@ -66,9 +66,9 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
if (nvHandle.AsgOp is not Operation handleOp || if (nvHandle.AsgOp is not Operation handleOp ||
handleOp.Inst != Instruction.Load || handleOp.Inst != Instruction.Load ||
handleOp.StorageKind != StorageKind.Input) (handleOp.StorageKind != StorageKind.Input && handleOp.StorageKind != StorageKind.StorageBuffer))
{ {
// Right now, we only allow bindless access when the handle comes from a shader input. // Right now, we only allow bindless access when the handle comes from a shader input or storage buffer.
// This is an artificial limitation to prevent it from being used in cases where it // This is an artificial limitation to prevent it from being used in cases where it
// would have a large performance impact of loading all textures in the pool. // would have a large performance impact of loading all textures in the pool.
// It might be removed in the future, if we can mitigate the performance impact. // It might be removed in the future, if we can mitigate the performance impact.

View File

@ -45,6 +45,8 @@ namespace Ryujinx.Graphics.Shader.Translation
public bool YNegateEnabled => _graphicsState.YNegateEnabled; public bool YNegateEnabled => _graphicsState.YNegateEnabled;
public bool OriginUpperLeft => _graphicsState.OriginUpperLeft; public bool OriginUpperLeft => _graphicsState.OriginUpperLeft;
public bool HalvePrimitiveId => _graphicsState.HalvePrimitiveId;
public ImapPixelType[] ImapTypes { get; } public ImapPixelType[] ImapTypes { get; }
public bool IaIndexing { get; private set; } public bool IaIndexing { get; private set; }
public bool OaIndexing { get; private set; } public bool OaIndexing { get; private set; }

View File

@ -691,6 +691,7 @@ namespace Ryujinx.Graphics.Vulkan
supportsBgraFormat: true, supportsBgraFormat: true,
supportsR4G4Format: false, supportsR4G4Format: false,
supportsR4G4B4A4Format: supportsR4G4B4A4Format, supportsR4G4B4A4Format: supportsR4G4B4A4Format,
supportsScaledVertexFormats: FormatCapabilities.SupportsScaledVertexFormats(),
supportsSnormBufferTextureFormat: true, supportsSnormBufferTextureFormat: true,
supports5BitComponentFormat: supports5BitComponentFormat, supports5BitComponentFormat: supports5BitComponentFormat,
supportsSparseBuffer: features2.Features.SparseBinding && mainQueueProperties.QueueFlags.HasFlag(QueueFlags.SparseBindingBit), supportsSparseBuffer: features2.Features.SparseBinding && mainQueueProperties.QueueFlags.HasFlag(QueueFlags.SparseBindingBit),
@ -705,7 +706,7 @@ namespace Ryujinx.Graphics.Vulkan
supportsMismatchingViewFormat: true, supportsMismatchingViewFormat: true,
supportsCubemapView: !IsAmdGcn, supportsCubemapView: !IsAmdGcn,
supportsNonConstantTextureOffset: false, supportsNonConstantTextureOffset: false,
supportsScaledVertexFormats: FormatCapabilities.SupportsScaledVertexFormats(), supportsQuads: false,
supportsSeparateSampler: true, supportsSeparateSampler: true,
supportsShaderBallot: false, supportsShaderBallot: false,
supportsShaderBarrierDivergence: Vendor != Vendor.Intel, supportsShaderBarrierDivergence: Vendor != Vendor.Intel,

View File

@ -1,6 +1,7 @@
using DiscordRPC; using DiscordRPC;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Configuration;
using System.Text;
namespace Ryujinx.UI.Common namespace Ryujinx.UI.Common
{ {
@ -9,6 +10,9 @@ namespace Ryujinx.UI.Common
private const string Description = "A simple, experimental Nintendo Switch emulator."; private const string Description = "A simple, experimental Nintendo Switch emulator.";
private const string ApplicationId = "1216775165866807456"; private const string ApplicationId = "1216775165866807456";
private const int ApplicationByteLimit = 128;
private const string Ellipsis = "…";
private static DiscordRpcClient _discordClient; private static DiscordRpcClient _discordClient;
private static RichPresence _discordPresenceMain; private static RichPresence _discordPresenceMain;
@ -60,18 +64,18 @@ namespace Ryujinx.UI.Common
} }
} }
public static void SwitchToPlayingState(string titleId, string titleName) public static void SwitchToPlayingState(string titleId, string applicationName)
{ {
_discordClient?.SetPresence(new RichPresence _discordClient?.SetPresence(new RichPresence
{ {
Assets = new Assets Assets = new Assets
{ {
LargeImageKey = "game", LargeImageKey = "game",
LargeImageText = titleName, LargeImageText = TruncateToByteLength(applicationName, ApplicationByteLimit),
SmallImageKey = "ryujinx", SmallImageKey = "ryujinx",
SmallImageText = Description, SmallImageText = Description,
}, },
Details = $"Playing {titleName}", Details = TruncateToByteLength($"Playing {applicationName}", ApplicationByteLimit),
State = (titleId == "0000000000000000") ? "Homebrew" : titleId.ToUpper(), State = (titleId == "0000000000000000") ? "Homebrew" : titleId.ToUpper(),
Timestamps = Timestamps.Now, Timestamps = Timestamps.Now,
Buttons = Buttons =
@ -90,6 +94,28 @@ namespace Ryujinx.UI.Common
_discordClient?.SetPresence(_discordPresenceMain); _discordClient?.SetPresence(_discordPresenceMain);
} }
private static string TruncateToByteLength(string input, int byteLimit)
{
if (Encoding.UTF8.GetByteCount(input) <= byteLimit)
{
return input;
}
// Find the length to trim the string to guarantee we have space for the trailing ellipsis.
int trimLimit = byteLimit - Encoding.UTF8.GetByteCount(Ellipsis);
// Basic trim to best case scenario of 1 byte characters.
input = input[..trimLimit];
while (Encoding.UTF8.GetByteCount(input) > trimLimit)
{
// Remove one character from the end of the string at a time.
input = input[..^1];
}
return input.TrimEnd() + Ellipsis;
}
public static void Exit() public static void Exit()
{ {
_discordClient?.Dispose(); _discordClient?.Dispose();

View File

@ -1,8 +1,10 @@
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Threading; using Avalonia.Threading;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
@ -84,7 +86,7 @@ namespace Ryujinx.Ava
ApplyConfiguredTheme(); ApplyConfiguredTheme();
} }
private void ApplyConfiguredTheme() public void ApplyConfiguredTheme()
{ {
try try
{ {
@ -92,13 +94,18 @@ namespace Ryujinx.Ava
if (string.IsNullOrWhiteSpace(baseStyle)) if (string.IsNullOrWhiteSpace(baseStyle))
{ {
ConfigurationState.Instance.UI.BaseStyle.Value = "Dark"; ConfigurationState.Instance.UI.BaseStyle.Value = "Auto";
baseStyle = ConfigurationState.Instance.UI.BaseStyle; baseStyle = ConfigurationState.Instance.UI.BaseStyle;
} }
ThemeVariant systemTheme = DetectSystemTheme();
ThemeManager.OnThemeChanged();
RequestedThemeVariant = baseStyle switch RequestedThemeVariant = baseStyle switch
{ {
"Auto" => systemTheme,
"Light" => ThemeVariant.Light, "Light" => ThemeVariant.Light,
"Dark" => ThemeVariant.Dark, "Dark" => ThemeVariant.Dark,
_ => ThemeVariant.Default, _ => ThemeVariant.Default,
@ -111,5 +118,28 @@ namespace Ryujinx.Ava
ShowRestartDialog(); ShowRestartDialog();
} }
} }
/// <summary>
/// Converts a PlatformThemeVariant value to the corresponding ThemeVariant value.
/// </summary>
public static ThemeVariant ConvertThemeVariant(PlatformThemeVariant platformThemeVariant) =>
platformThemeVariant switch
{
PlatformThemeVariant.Dark => ThemeVariant.Dark,
PlatformThemeVariant.Light => ThemeVariant.Light,
_ => ThemeVariant.Default,
};
public static ThemeVariant DetectSystemTheme()
{
if (Application.Current is App app)
{
var colorValues = app.PlatformSettings.GetColorValues();
return ConvertThemeVariant(colorValues.ThemeVariant);
}
return ThemeVariant.Default;
}
} }
} }

View File

@ -404,6 +404,7 @@
"GameListContextMenuToggleFavorite": "Toggle Favorite", "GameListContextMenuToggleFavorite": "Toggle Favorite",
"GameListContextMenuToggleFavoriteToolTip": "Toggle Favorite status of Game", "GameListContextMenuToggleFavoriteToolTip": "Toggle Favorite status of Game",
"SettingsTabGeneralTheme": "Theme:", "SettingsTabGeneralTheme": "Theme:",
"SettingsTabGeneralThemeAuto": "Auto",
"SettingsTabGeneralThemeDark": "Dark", "SettingsTabGeneralThemeDark": "Dark",
"SettingsTabGeneralThemeLight": "Light", "SettingsTabGeneralThemeLight": "Light",
"ControllerSettingsConfigureGeneral": "Configure", "ControllerSettingsConfigureGeneral": "Configure",

View File

@ -0,0 +1,14 @@
using System;
namespace Ryujinx.Ava.Common
{
public static class ThemeManager
{
public static event EventHandler ThemeChanged;
public static void OnThemeChanged()
{
ThemeChanged?.Invoke(null, EventArgs.Empty);
}
}
}

View File

@ -1,6 +1,8 @@
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Threading; using Avalonia.Threading;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Configuration;
@ -11,7 +13,7 @@ using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.ViewModels namespace Ryujinx.Ava.UI.ViewModels
{ {
public class AboutWindowViewModel : BaseModel public class AboutWindowViewModel : BaseModel, IDisposable
{ {
private Bitmap _githubLogo; private Bitmap _githubLogo;
private Bitmap _discordLogo; private Bitmap _discordLogo;
@ -86,23 +88,39 @@ namespace Ryujinx.Ava.UI.ViewModels
public AboutWindowViewModel() public AboutWindowViewModel()
{ {
Version = Program.Version; Version = Program.Version;
UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value);
if (ConfigurationState.Instance.UI.BaseStyle.Value == "Light")
{
GithubLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_GitHub_Light.png?assembly=Ryujinx.UI.Common")));
DiscordLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Discord_Light.png?assembly=Ryujinx.UI.Common")));
PatreonLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Patreon_Light.png?assembly=Ryujinx.UI.Common")));
TwitterLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Twitter_Light.png?assembly=Ryujinx.UI.Common")));
}
else
{
GithubLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_GitHub_Dark.png?assembly=Ryujinx.UI.Common")));
DiscordLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Discord_Dark.png?assembly=Ryujinx.UI.Common")));
PatreonLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Patreon_Dark.png?assembly=Ryujinx.UI.Common")));
TwitterLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Twitter_Dark.png?assembly=Ryujinx.UI.Common")));
}
Dispatcher.UIThread.InvokeAsync(DownloadPatronsJson); Dispatcher.UIThread.InvokeAsync(DownloadPatronsJson);
ThemeManager.ThemeChanged += ThemeManager_ThemeChanged;
}
private void ThemeManager_ThemeChanged(object sender, EventArgs e)
{
Dispatcher.UIThread.Post(() => UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value));
}
private void UpdateLogoTheme(string theme)
{
bool isDarkTheme = theme == "Dark" || (theme == "Auto" && App.DetectSystemTheme() == ThemeVariant.Dark);
string basePath = "resm:Ryujinx.UI.Common.Resources.";
string themeSuffix = isDarkTheme ? "Dark.png" : "Light.png";
GithubLogo = LoadBitmap($"{basePath}Logo_GitHub_{themeSuffix}?assembly=Ryujinx.UI.Common");
DiscordLogo = LoadBitmap($"{basePath}Logo_Discord_{themeSuffix}?assembly=Ryujinx.UI.Common");
PatreonLogo = LoadBitmap($"{basePath}Logo_Patreon_{themeSuffix}?assembly=Ryujinx.UI.Common");
TwitterLogo = LoadBitmap($"{basePath}Logo_Twitter_{themeSuffix}?assembly=Ryujinx.UI.Common");
}
private Bitmap LoadBitmap(string uri)
{
return new Bitmap(Avalonia.Platform.AssetLoader.Open(new Uri(uri)));
}
public void Dispose()
{
ThemeManager.ThemeChanged -= ThemeManager_ThemeChanged;
GC.SuppressFinalize(this);
} }
private async Task DownloadPatronsJson() private async Task DownloadPatronsJson()

View File

@ -397,7 +397,13 @@ namespace Ryujinx.Ava.UI.ViewModels
GameDirectories.Clear(); GameDirectories.Clear();
GameDirectories.AddRange(config.UI.GameDirs.Value); GameDirectories.AddRange(config.UI.GameDirs.Value);
BaseStyleIndex = config.UI.BaseStyle == "Light" ? 0 : 1; BaseStyleIndex = config.UI.BaseStyle.Value switch
{
"Auto" => 0,
"Light" => 1,
"Dark" => 2,
_ => 0
};
// Input // Input
EnableDockedMode = config.System.EnableDockedMode; EnableDockedMode = config.System.EnableDockedMode;
@ -486,7 +492,13 @@ namespace Ryujinx.Ava.UI.ViewModels
config.UI.GameDirs.Value = gameDirs; config.UI.GameDirs.Value = gameDirs;
} }
config.UI.BaseStyle.Value = BaseStyleIndex == 0 ? "Light" : "Dark"; config.UI.BaseStyle.Value = BaseStyleIndex switch
{
0 => "Auto",
1 => "Light",
2 => "Dark",
_ => "Auto"
};
// Input // Input
config.System.EnableDockedMode.Value = EnableDockedMode; config.System.EnableDockedMode.Value = EnableDockedMode;

View File

@ -65,6 +65,9 @@
<ComboBox SelectedIndex="{Binding BaseStyleIndex}" <ComboBox SelectedIndex="{Binding BaseStyleIndex}"
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"
MinWidth="100"> MinWidth="100">
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeAuto}" />
</ComboBoxItem>
<ComboBoxItem> <ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeLight}" /> <TextBlock Text="{locale:Locale SettingsTabGeneralThemeLight}" />
</ComboBoxItem> </ComboBoxItem>

View File

@ -2,6 +2,7 @@ using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common; using Ryujinx.Ava.Common;
@ -92,6 +93,29 @@ namespace Ryujinx.Ava.UI.Windows
} }
} }
/// <summary>
/// Event handler for detecting OS theme change when using "Follow OS theme" option
/// </summary>
private void OnPlatformColorValuesChanged(object sender, PlatformColorValues e)
{
if (Application.Current is App app)
{
app.ApplyConfiguredTheme();
}
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
if (PlatformSettings != null)
{
/// <summary>
/// Unsubscribe to the ColorValuesChanged event
/// </summary>
PlatformSettings.ColorValuesChanged -= OnPlatformColorValuesChanged;
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
@ -390,6 +414,11 @@ namespace Ryujinx.Ava.UI.Windows
Initialize(); Initialize();
/// <summary>
/// Subscribe to the ColorValuesChanged event
/// </summary>
PlatformSettings.ColorValuesChanged += OnPlatformColorValuesChanged;
ViewModel.Initialize( ViewModel.Initialize(
ContentManager, ContentManager,
StorageProvider, StorageProvider,