Compare commits
17 Commits
Canary-1.2
...
1d06d4f9b7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d06d4f9b7 | ||
|
|
3b447b764e | ||
|
|
849fd0199e | ||
|
|
57e26114e8 | ||
|
|
6a283190b3 | ||
|
|
85547874c8 | ||
|
|
16ca8e5005 | ||
|
|
cfa5ad0757 | ||
|
|
43ece083b2 | ||
|
|
e1f5c501b0 | ||
|
|
ffe366d953 | ||
|
|
75c7a29278 | ||
|
|
3a0d9c1435 | ||
|
|
93d1476a2a | ||
|
|
ce13830063 | ||
|
|
aa3f2824e0 | ||
|
|
d2bb580aea |
@@ -3,7 +3,6 @@ using ARMeilleure.CodeGen.Linking;
|
||||
using ARMeilleure.CodeGen.Unwinding;
|
||||
using ARMeilleure.Common;
|
||||
using ARMeilleure.Memory;
|
||||
using ARMeilleure.State;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
@@ -31,8 +30,8 @@ namespace ARMeilleure.Translation.PTC
|
||||
{
|
||||
private const string OuterHeaderMagicString = "PTCohd\0\0";
|
||||
private const string InnerHeaderMagicString = "PTCihd\0\0";
|
||||
|
||||
private const uint InternalVersion = 7007; //! To be incremented manually for each change to the ARMeilleure project.
|
||||
|
||||
private const uint InternalVersion = 6998; //! To be incremented manually for each change to the ARMeilleure project.
|
||||
|
||||
private const string ActualDir = "0";
|
||||
private const string BackupDir = "1";
|
||||
@@ -185,36 +184,6 @@ namespace ARMeilleure.Translation.PTC
|
||||
InitializeCarriers();
|
||||
}
|
||||
|
||||
private bool ContainsBlacklistedFunctions()
|
||||
{
|
||||
List<ulong> blacklist = Profiler.GetBlacklistedFunctions();
|
||||
bool containsBlacklistedFunctions = false;
|
||||
_infosStream.Seek(0L, SeekOrigin.Begin);
|
||||
bool foundBadFunction = false;
|
||||
|
||||
for (int index = 0; index < GetEntriesCount(); index++)
|
||||
{
|
||||
InfoEntry infoEntry = DeserializeStructure<InfoEntry>(_infosStream);
|
||||
foreach (ulong address in blacklist)
|
||||
{
|
||||
if (infoEntry.Address == address)
|
||||
{
|
||||
containsBlacklistedFunctions = true;
|
||||
Logger.Warning?.Print(LogClass.Ptc, "PPTC cache invalidated: Found blacklisted functions in PPTC cache");
|
||||
foundBadFunction = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundBadFunction)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return containsBlacklistedFunctions;
|
||||
}
|
||||
|
||||
private void PreLoad()
|
||||
{
|
||||
string fileNameActual = $"{CachePathActual}.cache";
|
||||
@@ -563,7 +532,7 @@ namespace ARMeilleure.Translation.PTC
|
||||
|
||||
public void LoadTranslations(Translator translator)
|
||||
{
|
||||
if (AreCarriersEmpty() || ContainsBlacklistedFunctions())
|
||||
if (AreCarriersEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -866,18 +835,10 @@ namespace ARMeilleure.Translation.PTC
|
||||
while (profiledFuncsToTranslate.TryDequeue(out (ulong address, PtcProfiler.FuncProfile funcProfile) item))
|
||||
{
|
||||
ulong address = item.address;
|
||||
ExecutionMode executionMode = item.funcProfile.Mode;
|
||||
bool highCq = item.funcProfile.HighCq;
|
||||
|
||||
Debug.Assert(Profiler.IsAddressInStaticCodeRange(address));
|
||||
|
||||
TranslatedFunction func = translator.Translate(address, executionMode, highCq);
|
||||
|
||||
if (func == null)
|
||||
{
|
||||
Profiler.UpdateEntry(address, executionMode, true, true);
|
||||
continue;
|
||||
}
|
||||
TranslatedFunction func = translator.Translate(address, item.funcProfile.Mode, item.funcProfile.HighCq);
|
||||
|
||||
bool isAddressUnique = translator.Functions.TryAdd(address, func.GuestSize, func);
|
||||
|
||||
@@ -924,14 +885,7 @@ namespace ARMeilleure.Translation.PTC
|
||||
|
||||
PtcStateChanged?.Invoke(PtcLoadingState.Loaded, _translateCount, _translateTotalCount);
|
||||
|
||||
if (_translateCount == _translateTotalCount)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Ptc, $"{_translateCount} of {_translateTotalCount} functions translated | Thread count: {degreeOfParallelism} in {sw.Elapsed.TotalSeconds} s");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Ptc, $"{_translateCount} of {_translateTotalCount} functions translated | {_translateTotalCount - _translateCount} function{(_translateTotalCount - _translateCount != 1 ? "s" : "")} blacklisted | Thread count: {degreeOfParallelism} in {sw.Elapsed.TotalSeconds} s");
|
||||
}
|
||||
Logger.Info?.Print(LogClass.Ptc, $"{_translateCount} of {_translateTotalCount} functions translated | Thread count: {degreeOfParallelism} in {sw.Elapsed.TotalSeconds} s");
|
||||
|
||||
Thread preSaveThread = new(PreSave)
|
||||
{
|
||||
|
||||
@@ -24,12 +24,11 @@ namespace ARMeilleure.Translation.PTC
|
||||
{
|
||||
private const string OuterHeaderMagicString = "Pohd\0\0\0\0";
|
||||
|
||||
private const uint InternalVersion = 7007; //! Not to be incremented manually for each change to the ARMeilleure project.
|
||||
private const uint InternalVersion = 5518; //! Not to be incremented manually for each change to the ARMeilleure project.
|
||||
|
||||
private static readonly uint[] _migrateInternalVersions =
|
||||
private static readonly uint[] _migrateInternalVersions =
|
||||
[
|
||||
1866,
|
||||
5518,
|
||||
1866
|
||||
];
|
||||
|
||||
private const int SaveInterval = 30; // Seconds.
|
||||
@@ -78,30 +77,20 @@ namespace ARMeilleure.Translation.PTC
|
||||
private void TimerElapsed(object _, ElapsedEventArgs __)
|
||||
=> new Thread(PreSave) { Name = "Ptc.DiskWriter" }.Start();
|
||||
|
||||
public void AddEntry(ulong address, ExecutionMode mode, bool highCq, bool blacklist = false)
|
||||
public void AddEntry(ulong address, ExecutionMode mode, bool highCq)
|
||||
{
|
||||
if (IsAddressInStaticCodeRange(address))
|
||||
{
|
||||
Debug.Assert(!highCq);
|
||||
|
||||
if (blacklist)
|
||||
lock (_lock)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ProfiledFuncs[address] = new FuncProfile(mode, highCq: false, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ProfiledFuncs.TryAdd(address, new FuncProfile(mode, highCq: false, false));
|
||||
}
|
||||
ProfiledFuncs.TryAdd(address, new FuncProfile(mode, highCq: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateEntry(ulong address, ExecutionMode mode, bool highCq, bool? blacklist = null)
|
||||
public void UpdateEntry(ulong address, ExecutionMode mode, bool highCq)
|
||||
{
|
||||
if (IsAddressInStaticCodeRange(address))
|
||||
{
|
||||
@@ -111,7 +100,7 @@ namespace ARMeilleure.Translation.PTC
|
||||
{
|
||||
Debug.Assert(ProfiledFuncs.ContainsKey(address));
|
||||
|
||||
ProfiledFuncs[address] = new FuncProfile(mode, highCq: true, blacklist ?? ProfiledFuncs[address].Blacklist);
|
||||
ProfiledFuncs[address] = new FuncProfile(mode, highCq: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,7 +116,7 @@ namespace ARMeilleure.Translation.PTC
|
||||
|
||||
foreach (KeyValuePair<ulong, FuncProfile> profiledFunc in ProfiledFuncs)
|
||||
{
|
||||
if (!funcs.ContainsKey(profiledFunc.Key) && !profiledFunc.Value.Blacklist)
|
||||
if (!funcs.ContainsKey(profiledFunc.Key))
|
||||
{
|
||||
profiledFuncsToTranslate.Enqueue((profiledFunc.Key, profiledFunc.Value));
|
||||
}
|
||||
@@ -142,24 +131,6 @@ namespace ARMeilleure.Translation.PTC
|
||||
ProfiledFuncs.TrimExcess();
|
||||
}
|
||||
|
||||
public List<ulong> GetBlacklistedFunctions()
|
||||
{
|
||||
List<ulong> funcs = new List<ulong>();
|
||||
|
||||
foreach (var profiledFunc in ProfiledFuncs)
|
||||
{
|
||||
if (profiledFunc.Value.Blacklist)
|
||||
{
|
||||
if (!funcs.Contains(profiledFunc.Key))
|
||||
{
|
||||
funcs.Add(profiledFunc.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return funcs;
|
||||
}
|
||||
|
||||
public void PreLoad()
|
||||
{
|
||||
_lastHash = default;
|
||||
@@ -250,18 +221,13 @@ namespace ARMeilleure.Translation.PTC
|
||||
return false;
|
||||
}
|
||||
|
||||
Func<ulong, FuncProfile, (ulong, FuncProfile)> migrateEntryFunc = null;
|
||||
|
||||
switch (outerHeader.InfoFileVersion)
|
||||
{
|
||||
case InternalVersion:
|
||||
ProfiledFuncs = Deserialize(stream);
|
||||
break;
|
||||
case 1866:
|
||||
migrateEntryFunc = (address, profile) => (address + 0x500000UL, profile);
|
||||
goto case 5518;
|
||||
case 5518:
|
||||
ProfiledFuncs = DeserializeAddBlacklist(stream, migrateEntryFunc);
|
||||
ProfiledFuncs = Deserialize(stream, (address, profile) => (address + 0x500000UL, profile));
|
||||
break;
|
||||
default:
|
||||
Logger.Error?.Print(LogClass.Ptc, $"No migration path for {nameof(outerHeader.InfoFileVersion)} '{outerHeader.InfoFileVersion}'. Discarding cache.");
|
||||
@@ -291,16 +257,6 @@ namespace ARMeilleure.Translation.PTC
|
||||
return DeserializeDictionary<ulong, FuncProfile>(stream, DeserializeStructure<FuncProfile>);
|
||||
}
|
||||
|
||||
private static Dictionary<ulong, FuncProfile> DeserializeAddBlacklist(Stream stream, Func<ulong, FuncProfile, (ulong, FuncProfile)> migrateEntryFunc = null)
|
||||
{
|
||||
if (migrateEntryFunc != null)
|
||||
{
|
||||
return DeserializeAndUpdateDictionary(stream, (Stream stream) => { return new FuncProfile(DeserializeStructure<FuncProfilePreBlacklist>(stream)); }, migrateEntryFunc);
|
||||
}
|
||||
|
||||
return DeserializeDictionary<ulong, FuncProfile>(stream, (Stream stream) => { return new FuncProfile(DeserializeStructure<FuncProfilePreBlacklist>(stream)); });
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<byte> GetReadOnlySpan(MemoryStream memoryStream)
|
||||
{
|
||||
return new(memoryStream.GetBuffer(), (int)memoryStream.Position, (int)memoryStream.Length - (int)memoryStream.Position);
|
||||
@@ -432,35 +388,13 @@ namespace ARMeilleure.Translation.PTC
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 6*/)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 5*/)]
|
||||
public struct FuncProfile
|
||||
{
|
||||
public ExecutionMode Mode;
|
||||
public bool HighCq;
|
||||
public bool Blacklist;
|
||||
|
||||
public FuncProfile(ExecutionMode mode, bool highCq, bool blacklist)
|
||||
{
|
||||
Mode = mode;
|
||||
HighCq = highCq;
|
||||
Blacklist = blacklist;
|
||||
}
|
||||
|
||||
public FuncProfile(FuncProfilePreBlacklist fp)
|
||||
{
|
||||
Mode = fp.Mode;
|
||||
HighCq = fp.HighCq;
|
||||
Blacklist = false;
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 5*/)]
|
||||
public struct FuncProfilePreBlacklist
|
||||
{
|
||||
public ExecutionMode Mode;
|
||||
public bool HighCq;
|
||||
|
||||
public FuncProfilePreBlacklist(ExecutionMode mode, bool highCq)
|
||||
public FuncProfile(ExecutionMode mode, bool highCq)
|
||||
{
|
||||
Mode = mode;
|
||||
HighCq = highCq;
|
||||
|
||||
@@ -249,11 +249,6 @@ namespace ARMeilleure.Translation
|
||||
|
||||
ControlFlowGraph cfg = EmitAndGetCFG(context, blocks, out Range funcRange, out Counter<uint> counter);
|
||||
|
||||
if (cfg == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
ulong funcSize = funcRange.End - funcRange.Start;
|
||||
|
||||
Logger.EndPass(PassName.Translation, cfg);
|
||||
@@ -412,11 +407,6 @@ namespace ARMeilleure.Translation
|
||||
if (opCode.Instruction.Emitter != null)
|
||||
{
|
||||
opCode.Instruction.Emitter(context);
|
||||
if (opCode.Instruction.Name == InstName.Und && blkIndex == 0)
|
||||
{
|
||||
range = new Range(rangeStart, rangeEnd);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -192,7 +192,6 @@ namespace Ryujinx.HLE
|
||||
/// <summary>
|
||||
/// The desired hacky workarounds.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
public EnabledDirtyHack[] Hacks { internal get; set; }
|
||||
|
||||
public HLEConfiguration(VirtualFileSystem virtualFileSystem,
|
||||
|
||||
@@ -20,7 +20,6 @@ namespace Ryujinx.HLE.HOS
|
||||
private readonly string _titleIdText;
|
||||
private readonly string _displayVersion;
|
||||
private readonly bool _diskCacheEnabled;
|
||||
private readonly string _diskCacheSelector;
|
||||
private readonly ulong _codeAddress;
|
||||
private readonly ulong _codeSize;
|
||||
|
||||
@@ -32,7 +31,6 @@ namespace Ryujinx.HLE.HOS
|
||||
string titleIdText,
|
||||
string displayVersion,
|
||||
bool diskCacheEnabled,
|
||||
string diskCacheSelector,
|
||||
ulong codeAddress,
|
||||
ulong codeSize)
|
||||
{
|
||||
@@ -41,7 +39,6 @@ namespace Ryujinx.HLE.HOS
|
||||
_titleIdText = titleIdText;
|
||||
_displayVersion = displayVersion;
|
||||
_diskCacheEnabled = diskCacheEnabled;
|
||||
_diskCacheSelector = diskCacheSelector;
|
||||
_codeAddress = codeAddress;
|
||||
_codeSize = codeSize;
|
||||
}
|
||||
@@ -117,7 +114,7 @@ namespace Ryujinx.HLE.HOS
|
||||
}
|
||||
}
|
||||
|
||||
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, _diskCacheSelector ?? "default");
|
||||
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, "default"); //Ready for exefs profiles
|
||||
|
||||
return processContext;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ using LibHac.Loader;
|
||||
using LibHac.Tools.Fs;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.RomFs;
|
||||
using LibHac.Util;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
@@ -20,7 +19,6 @@ using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using LazyFile = Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy.LazyFile;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
@@ -583,7 +581,6 @@ namespace Ryujinx.HLE.HOS
|
||||
public BitVector32 Stubs;
|
||||
public BitVector32 Replaces;
|
||||
public MetaLoader Npdm;
|
||||
public string Hash;
|
||||
|
||||
public bool Modified => (Stubs.Data | Replaces.Data) != 0;
|
||||
}
|
||||
@@ -594,11 +591,8 @@ namespace Ryujinx.HLE.HOS
|
||||
{
|
||||
Stubs = new BitVector32(),
|
||||
Replaces = new BitVector32(),
|
||||
Hash = null,
|
||||
};
|
||||
|
||||
string tempHash = string.Empty;
|
||||
|
||||
if (!_appMods.TryGetValue(applicationId, out ModCache mods) || mods.ExefsDirs.Count == 0)
|
||||
{
|
||||
return modLoadResult;
|
||||
@@ -634,16 +628,8 @@ namespace Ryujinx.HLE.HOS
|
||||
|
||||
modLoadResult.Replaces[1 << i] = true;
|
||||
|
||||
using (FileStream stream = nsoFile.OpenRead())
|
||||
{
|
||||
nsos[i] = new NsoExecutable(stream.AsStorage(), nsoName);
|
||||
Logger.Info?.Print(LogClass.ModLoader, $"NSO '{nsoName}' replaced");
|
||||
using (MD5 md5 = MD5.Create())
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
tempHash += BitConverter.ToString(md5.ComputeHash(stream)).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
nsos[i] = new NsoExecutable(nsoFile.OpenRead().AsStorage(), nsoName);
|
||||
Logger.Info?.Print(LogClass.ModLoader, $"NSO '{nsoName}' replaced");
|
||||
}
|
||||
|
||||
modLoadResult.Stubs[1 << i] |= File.Exists(Path.Combine(mod.Path.FullName, nsoName + StubExtension));
|
||||
@@ -675,14 +661,6 @@ namespace Ryujinx.HLE.HOS
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(tempHash))
|
||||
{
|
||||
using (MD5 md5 = MD5.Create())
|
||||
{
|
||||
modLoadResult.Hash += BitConverter.ToString(md5.ComputeHash(tempHash.ToBytes())).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
return modLoadResult;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,13 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
// Apply Nsos patches.
|
||||
device.Configuration.VirtualFileSystem.ModLoader.ApplyNsoPatches(programId, nsoExecutables);
|
||||
|
||||
// Don't use PTC if ExeFS files have been replaced.
|
||||
bool enablePtc = device.System.EnablePtc && !modLoadResult.Modified;
|
||||
if (!enablePtc)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Ptc, "Detected unsupported ExeFs modifications. PTC disabled.");
|
||||
}
|
||||
|
||||
string programName = string.Empty;
|
||||
|
||||
if (!isHomebrew && programId > 0x010000000000FFFF)
|
||||
@@ -110,8 +117,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
device.System.KernelContext,
|
||||
metaLoader,
|
||||
nacpData,
|
||||
device.System.EnablePtc,
|
||||
modLoadResult.Hash,
|
||||
enablePtc,
|
||||
true,
|
||||
programName,
|
||||
metaLoader.GetProgramId(),
|
||||
|
||||
@@ -235,7 +235,6 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
dummyExeFs.GetNpdm(),
|
||||
nacpData,
|
||||
diskCacheEnabled: false,
|
||||
diskCacheSelector: null,
|
||||
allowCodeMemoryForJit: true,
|
||||
programName,
|
||||
programId,
|
||||
|
||||
@@ -186,7 +186,6 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
false,
|
||||
null,
|
||||
codeAddress,
|
||||
codeSize);
|
||||
|
||||
@@ -227,7 +226,6 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
MetaLoader metaLoader,
|
||||
BlitStruct<ApplicationControlProperty> applicationControlProperties,
|
||||
bool diskCacheEnabled,
|
||||
string diskCacheSelector,
|
||||
bool allowCodeMemoryForJit,
|
||||
string name,
|
||||
ulong programId,
|
||||
@@ -381,7 +379,6 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
$"{programId:x16}",
|
||||
displayVersion,
|
||||
diskCacheEnabled,
|
||||
diskCacheSelector,
|
||||
codeStart,
|
||||
codeSize);
|
||||
|
||||
|
||||
@@ -2022,56 +2022,6 @@
|
||||
"zh_TW": "下一次啟動遊戲時,觸發 PPTC 進行重建"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListContextMenuCacheManagementNukePptc",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Purge PPTC cache",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListContextMenuCacheManagementNukePptcToolTip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Deletes all PPTC cache files for the Application",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "GameListContextMenuCacheManagementPurgeShaderCache",
|
||||
"Translations": {
|
||||
@@ -12997,31 +12947,6 @@
|
||||
"zh_TW": "在 {0} 清除 PPTC 快取時出錯: {1}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DialogPPTCNukeMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DialogShaderDeletionMessage",
|
||||
"Translations": {
|
||||
|
||||
@@ -5,7 +5,6 @@ using Ryujinx.Ava.Utilities.Configuration;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.GraphicsDriver;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Logging.Targets;
|
||||
@@ -27,7 +26,6 @@ using Ryujinx.SDL2.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Headless
|
||||
@@ -288,9 +286,6 @@ namespace Ryujinx.Headless
|
||||
GraphicsConfig.EnableMacroHLE = !option.DisableMacroHLE;
|
||||
|
||||
DriverUtilities.InitDriverConfig(option.BackendThreading == BackendThreading.Off);
|
||||
|
||||
if (_inputConfiguration.OfType<StandardControllerInputConfig>().Any(ic => ic.Led.UseRainbow))
|
||||
Rainbow.Enable();
|
||||
|
||||
while (true)
|
||||
{
|
||||
|
||||
@@ -81,11 +81,6 @@
|
||||
Header="{ext:Locale GameListContextMenuCacheManagementPurgePptc}"
|
||||
Icon="{ext:Icon mdi-refresh}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" />
|
||||
<MenuItem
|
||||
Click="NukePtcCache_Click"
|
||||
Header="{ext:Locale GameListContextMenuCacheManagementNukePptc}"
|
||||
Icon="{ext:Icon mdi-delete-alert}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuCacheManagementNukePptcToolTip}" />
|
||||
<MenuItem
|
||||
Click="PurgeShaderCache_Click"
|
||||
Header="{ext:Locale GameListContextMenuCacheManagementPurgeShaderCache}"
|
||||
|
||||
@@ -175,52 +175,6 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
}
|
||||
}
|
||||
|
||||
public async void NukePtcCache_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
||||
return;
|
||||
|
||||
UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogWarning],
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCNukeMessage, viewModel.SelectedApplication.Name)
|
||||
);
|
||||
|
||||
if (result == UserResult.Yes)
|
||||
{
|
||||
DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0"));
|
||||
DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1"));
|
||||
|
||||
List<FileInfo> cacheFiles = new();
|
||||
|
||||
if (mainDir.Exists)
|
||||
{
|
||||
cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache"));
|
||||
cacheFiles.AddRange(mainDir.EnumerateFiles("*.info"));
|
||||
}
|
||||
|
||||
if (backupDir.Exists)
|
||||
{
|
||||
cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache"));
|
||||
cacheFiles.AddRange(mainDir.EnumerateFiles("*.info"));
|
||||
}
|
||||
|
||||
if (cacheFiles.Count > 0)
|
||||
{
|
||||
foreach (FileInfo file in cacheFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
file.Delete();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, file.Name, ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async void PurgeShaderCache_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
|
||||
|
||||
260
src/Ryujinx/UI/Models/Input/StickVisualizer.cs
Normal file
260
src/Ryujinx/UI/Models/Input/StickVisualizer.cs
Normal file
@@ -0,0 +1,260 @@
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.UI.ViewModels.Input;
|
||||
using Ryujinx.Input;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models.Input
|
||||
{
|
||||
public class StickVisualizer : BaseModel, IDisposable
|
||||
{
|
||||
public const int DrawStickPollRate = 50; // Milliseconds per poll.
|
||||
public const int DrawStickCircumference = 5;
|
||||
public const float DrawStickScaleFactor = DrawStickCanvasCenter;
|
||||
public const int DrawStickCanvasSize = 100;
|
||||
public const int DrawStickBorderSize = DrawStickCanvasSize + 5;
|
||||
public const float DrawStickCanvasCenter = (DrawStickCanvasSize - DrawStickCircumference) / 2;
|
||||
public const float MaxVectorLength = DrawStickCanvasSize / 2;
|
||||
|
||||
public CancellationTokenSource PollTokenSource;
|
||||
public CancellationToken PollToken;
|
||||
|
||||
private static float _vectorLength;
|
||||
private static float _vectorMultiplier;
|
||||
|
||||
private bool disposedValue;
|
||||
|
||||
private DeviceType _type;
|
||||
public DeviceType Type
|
||||
{
|
||||
get => _type;
|
||||
set
|
||||
{
|
||||
_type = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private GamepadInputConfig _gamepadConfig;
|
||||
public GamepadInputConfig GamepadConfig
|
||||
{
|
||||
get => _gamepadConfig;
|
||||
set
|
||||
{
|
||||
_gamepadConfig = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private KeyboardInputConfig _keyboardConfig;
|
||||
public KeyboardInputConfig KeyboardConfig
|
||||
{
|
||||
get => _keyboardConfig;
|
||||
set
|
||||
{
|
||||
_keyboardConfig = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private (float, float) _uiStickLeft;
|
||||
public (float, float) UiStickLeft
|
||||
{
|
||||
get => (_uiStickLeft.Item1 * DrawStickScaleFactor, _uiStickLeft.Item2 * DrawStickScaleFactor);
|
||||
set
|
||||
{
|
||||
_uiStickLeft = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(UiStickRightX));
|
||||
OnPropertyChanged(nameof(UiStickRightY));
|
||||
OnPropertyChanged(nameof(UiDeadzoneRight));
|
||||
}
|
||||
}
|
||||
|
||||
private (float, float) _uiStickRight;
|
||||
public (float, float) UiStickRight
|
||||
{
|
||||
get => (_uiStickRight.Item1 * DrawStickScaleFactor, _uiStickRight.Item2 * DrawStickScaleFactor);
|
||||
set
|
||||
{
|
||||
_uiStickRight = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(UiStickLeftX));
|
||||
OnPropertyChanged(nameof(UiStickLeftY));
|
||||
OnPropertyChanged(nameof(UiDeadzoneLeft));
|
||||
}
|
||||
}
|
||||
|
||||
public float UiStickLeftX => ClampVector(UiStickLeft).Item1;
|
||||
public float UiStickLeftY => ClampVector(UiStickLeft).Item2;
|
||||
public float UiStickRightX => ClampVector(UiStickRight).Item1;
|
||||
public float UiStickRightY => ClampVector(UiStickRight).Item2;
|
||||
|
||||
public int UiStickCircumference => DrawStickCircumference;
|
||||
public int UiCanvasSize => DrawStickCanvasSize;
|
||||
public int UiStickBorderSize => DrawStickBorderSize;
|
||||
|
||||
public float? UiDeadzoneLeft => _gamepadConfig?.DeadzoneLeft * DrawStickCanvasSize - DrawStickCircumference;
|
||||
public float? UiDeadzoneRight => _gamepadConfig?.DeadzoneRight * DrawStickCanvasSize - DrawStickCircumference;
|
||||
|
||||
private InputViewModel Parent;
|
||||
|
||||
public StickVisualizer(InputViewModel parent)
|
||||
{
|
||||
Parent = parent;
|
||||
|
||||
PollTokenSource = new CancellationTokenSource();
|
||||
PollToken = PollTokenSource.Token;
|
||||
|
||||
Task.Run(Initialize, PollToken);
|
||||
}
|
||||
|
||||
public void UpdateConfig(object config)
|
||||
{
|
||||
if (config is ControllerInputViewModel padConfig)
|
||||
{
|
||||
GamepadConfig = padConfig.Config;
|
||||
Type = DeviceType.Controller;
|
||||
|
||||
return;
|
||||
}
|
||||
else if (config is KeyboardInputViewModel keyConfig)
|
||||
{
|
||||
KeyboardConfig = keyConfig.Config;
|
||||
Type = DeviceType.Keyboard;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Type = DeviceType.None;
|
||||
}
|
||||
|
||||
public async Task Initialize()
|
||||
{
|
||||
(float, float) leftBuffer;
|
||||
(float, float) rightBuffer;
|
||||
|
||||
while (!PollToken.IsCancellationRequested)
|
||||
{
|
||||
leftBuffer = (0f, 0f);
|
||||
rightBuffer = (0f, 0f);
|
||||
|
||||
switch (Type)
|
||||
{
|
||||
case DeviceType.Keyboard:
|
||||
IKeyboard keyboard = (IKeyboard)Parent.AvaloniaKeyboardDriver.GetGamepad("0");
|
||||
|
||||
if (keyboard != null)
|
||||
{
|
||||
KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot();
|
||||
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickRight))
|
||||
{
|
||||
leftBuffer.Item1 += 1;
|
||||
}
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickLeft))
|
||||
{
|
||||
leftBuffer.Item1 -= 1;
|
||||
}
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickUp))
|
||||
{
|
||||
leftBuffer.Item2 += 1;
|
||||
}
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickDown))
|
||||
{
|
||||
leftBuffer.Item2 -= 1;
|
||||
}
|
||||
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickRight))
|
||||
{
|
||||
rightBuffer.Item1 += 1;
|
||||
}
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickLeft))
|
||||
{
|
||||
rightBuffer.Item1 -= 1;
|
||||
}
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickUp))
|
||||
{
|
||||
rightBuffer.Item2 += 1;
|
||||
}
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickDown))
|
||||
{
|
||||
rightBuffer.Item2 -= 1;
|
||||
}
|
||||
|
||||
UiStickLeft = leftBuffer;
|
||||
UiStickRight = rightBuffer;
|
||||
}
|
||||
break;
|
||||
|
||||
case DeviceType.Controller:
|
||||
IGamepad controller = Parent.SelectedGamepad;
|
||||
|
||||
if (controller != null)
|
||||
{
|
||||
leftBuffer = controller.GetStick((StickInputId)GamepadConfig.LeftJoystick);
|
||||
rightBuffer = controller.GetStick((StickInputId)GamepadConfig.RightJoystick);
|
||||
}
|
||||
break;
|
||||
|
||||
case DeviceType.None:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Unable to poll device type \"{Type}\"");
|
||||
}
|
||||
|
||||
UiStickLeft = leftBuffer;
|
||||
UiStickRight = rightBuffer;
|
||||
|
||||
await Task.Delay(DrawStickPollRate, PollToken);
|
||||
}
|
||||
|
||||
PollTokenSource.Dispose();
|
||||
}
|
||||
|
||||
public static (float, float) ClampVector((float, float) vect)
|
||||
{
|
||||
_vectorMultiplier = 1;
|
||||
_vectorLength = MathF.Sqrt((vect.Item1 * vect.Item1) + (vect.Item2 * vect.Item2));
|
||||
|
||||
if (_vectorLength > MaxVectorLength)
|
||||
{
|
||||
_vectorMultiplier = MaxVectorLength / _vectorLength;
|
||||
}
|
||||
|
||||
vect.Item1 = vect.Item1 * _vectorMultiplier + DrawStickCanvasCenter;
|
||||
vect.Item2 = vect.Item2 * _vectorMultiplier + DrawStickCanvasCenter;
|
||||
|
||||
return vect;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
PollTokenSource.Cancel();
|
||||
}
|
||||
|
||||
KeyboardConfig = null;
|
||||
GamepadConfig = null;
|
||||
Parent = null;
|
||||
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.Input;
|
||||
using Ryujinx.Ava.UI.Models.Input;
|
||||
using Ryujinx.Ava.UI.Views.Input;
|
||||
using Ryujinx.UI.Views.Input;
|
||||
@@ -11,8 +12,30 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
{
|
||||
public partial class ControllerInputViewModel : BaseModel
|
||||
{
|
||||
[ObservableProperty] private GamepadInputConfig _config;
|
||||
private GamepadInputConfig _config;
|
||||
public GamepadInputConfig Config
|
||||
{
|
||||
get => _config;
|
||||
set
|
||||
{
|
||||
_config = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private StickVisualizer _visualizer;
|
||||
public StickVisualizer Visualizer
|
||||
{
|
||||
get => _visualizer;
|
||||
set
|
||||
{
|
||||
_visualizer = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isLeft;
|
||||
public bool IsLeft
|
||||
{
|
||||
@@ -38,14 +61,15 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
}
|
||||
|
||||
public bool HasSides => IsLeft ^ IsRight;
|
||||
|
||||
|
||||
[ObservableProperty] private SvgImage _image;
|
||||
|
||||
|
||||
public InputViewModel ParentModel { get; }
|
||||
|
||||
public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config)
|
||||
|
||||
public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config, StickVisualizer visualizer)
|
||||
{
|
||||
ParentModel = model;
|
||||
Visualizer = visualizer;
|
||||
model.NotifyChangesEvent += OnParentModelChanged;
|
||||
OnParentModelChanged();
|
||||
Config = config;
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
private int _controller;
|
||||
private string _controllerImage;
|
||||
private int _device;
|
||||
[ObservableProperty] private object _configViewModel;
|
||||
private object _configViewModel;
|
||||
[ObservableProperty] private string _profileName;
|
||||
private bool _isLoaded;
|
||||
|
||||
@@ -67,6 +67,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
OnPropertiesChanged(nameof(HasLed), nameof(CanClearLed));
|
||||
}
|
||||
}
|
||||
public StickVisualizer VisualStick { get; private set; }
|
||||
|
||||
public ObservableCollection<PlayerModel> PlayerIndexes { get; set; }
|
||||
public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; }
|
||||
@@ -87,6 +88,19 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
public bool IsModified { get; set; }
|
||||
public event Action NotifyChangesEvent;
|
||||
|
||||
public object ConfigViewModel
|
||||
{
|
||||
get => _configViewModel;
|
||||
set
|
||||
{
|
||||
_configViewModel = value;
|
||||
|
||||
VisualStick.UpdateConfig(value);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerIndex PlayerIdChoose
|
||||
{
|
||||
get => _playerIdChoose;
|
||||
@@ -262,6 +276,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
Devices = [];
|
||||
ProfilesList = [];
|
||||
DeviceList = [];
|
||||
VisualStick = new StickVisualizer(this);
|
||||
|
||||
ControllerImage = ProControllerResource;
|
||||
|
||||
@@ -282,12 +297,12 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
if (Config is StandardKeyboardInputConfig keyboardInputConfig)
|
||||
{
|
||||
ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig));
|
||||
ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig), VisualStick);
|
||||
}
|
||||
|
||||
if (Config is StandardControllerInputConfig controllerInputConfig)
|
||||
{
|
||||
ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig));
|
||||
ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig), VisualStick);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -886,6 +901,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
_mainWindow.ViewModel.AppHost?.NpadManager.UnblockInputUpdates();
|
||||
|
||||
VisualStick.Dispose();
|
||||
|
||||
SelectedGamepad?.Dispose();
|
||||
|
||||
AvaloniaKeyboardDriver.Dispose();
|
||||
|
||||
@@ -6,7 +6,29 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
{
|
||||
public partial class KeyboardInputViewModel : BaseModel
|
||||
{
|
||||
[ObservableProperty] private KeyboardInputConfig _config;
|
||||
private KeyboardInputConfig _config;
|
||||
public KeyboardInputConfig Config
|
||||
{
|
||||
get => _config;
|
||||
set
|
||||
{
|
||||
_config = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private StickVisualizer _visualizer;
|
||||
public StickVisualizer Visualizer
|
||||
{
|
||||
get => _visualizer;
|
||||
set
|
||||
{
|
||||
_visualizer = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isLeft;
|
||||
public bool IsLeft
|
||||
@@ -38,9 +60,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
public readonly InputViewModel ParentModel;
|
||||
|
||||
public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config)
|
||||
public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config, StickVisualizer visualizer)
|
||||
{
|
||||
ParentModel = model;
|
||||
Visualizer = visualizer;
|
||||
model.NotifyChangesEvent += OnParentModelChanged;
|
||||
OnParentModelChanged();
|
||||
Config = config;
|
||||
|
||||
@@ -319,17 +319,103 @@
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<!-- Controller Picture -->
|
||||
<Image
|
||||
Margin="0,10"
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Source="{Binding Image}" />
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Margin="0,10"
|
||||
MinHeight="90">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<Image
|
||||
Margin="5,10"
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Source="{Binding Image}" />
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
Orientation="Horizontal"
|
||||
Spacing="20"
|
||||
HorizontalAlignment="Center">
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Height="{Binding Visualizer.UiStickBorderSize}"
|
||||
Width="{Binding Visualizer.UiStickBorderSize}"
|
||||
IsVisible="{Binding IsLeft}">
|
||||
<Canvas
|
||||
Background="{DynamicResource ThemeBackgroundColor}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}">
|
||||
<Grid
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Background="{DynamicResource ThemeBackgroundColor}">
|
||||
<Ellipse
|
||||
HorizontalAlignment="Center"
|
||||
Stroke="Black"
|
||||
StrokeThickness="1"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}" />
|
||||
<Ellipse
|
||||
HorizontalAlignment="Center"
|
||||
Fill="Gray"
|
||||
Opacity="100"
|
||||
Height="{Binding Visualizer.UiDeadzoneLeft}"
|
||||
Width="{Binding Visualizer.UiDeadzoneLeft}" />
|
||||
</Grid>
|
||||
<Ellipse
|
||||
Fill="Red"
|
||||
Width="{Binding Visualizer.UiStickCircumference}"
|
||||
Height="{Binding Visualizer.UiStickCircumference}"
|
||||
Canvas.Bottom="{Binding Visualizer.UiStickLeftY}"
|
||||
Canvas.Left="{Binding Visualizer.UiStickLeftX}" />
|
||||
</Canvas>
|
||||
</Border>
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Height="{Binding Visualizer.UiStickBorderSize}"
|
||||
Width="{Binding Visualizer.UiStickBorderSize}"
|
||||
IsVisible="{Binding IsRight}">
|
||||
<Canvas
|
||||
Background="{DynamicResource ThemeBackgroundColor}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}">
|
||||
<Grid
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Background="{DynamicResource ThemeBackgroundColor}">
|
||||
<Ellipse
|
||||
HorizontalAlignment="Center"
|
||||
Stroke="Black"
|
||||
StrokeThickness="1"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}" />
|
||||
<Ellipse
|
||||
HorizontalAlignment="Center"
|
||||
Fill="Gray"
|
||||
Opacity="100"
|
||||
Height="{Binding Visualizer.UiDeadzoneRight}"
|
||||
Width="{Binding Visualizer.UiDeadzoneRight}" />
|
||||
</Grid>
|
||||
<Ellipse
|
||||
Fill="Red"
|
||||
Width="{Binding Visualizer.UiStickCircumference}"
|
||||
Height="{Binding Visualizer.UiStickCircumference}"
|
||||
Canvas.Bottom="{Binding Visualizer.UiStickRightY}"
|
||||
Canvas.Left="{Binding Visualizer.UiStickRightX}" />
|
||||
</Canvas>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5">
|
||||
<StackPanel
|
||||
Margin="8"
|
||||
Orientation="Vertical">
|
||||
@@ -348,8 +434,8 @@
|
||||
Minimum="0"
|
||||
Value="{Binding Config.TriggerThreshold, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Width="25"
|
||||
Text="{Binding Config.TriggerThreshold, StringFormat=\{0:0.00\}}" />
|
||||
Width="25"
|
||||
Text="{Binding Config.TriggerThreshold, StringFormat=\{0:0.00\}}" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Orientation="Vertical"
|
||||
|
||||
@@ -312,12 +312,79 @@
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<!-- Controller Picture -->
|
||||
<Image
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Margin="0,10"
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Source="{Binding Image}" />
|
||||
MinHeight="90">
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
Orientation="Horizontal"
|
||||
Spacing="20"
|
||||
HorizontalAlignment="Center">
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Height="{Binding Visualizer.UiStickBorderSize}"
|
||||
Width="{Binding Visualizer.UiStickBorderSize}"
|
||||
IsVisible="{Binding IsLeft}">
|
||||
<Canvas
|
||||
Background="{DynamicResource ThemeBackgroundColor}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}">
|
||||
<Grid
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Background="{DynamicResource ThemeBackgroundColor}">
|
||||
<Ellipse
|
||||
HorizontalAlignment="Center"
|
||||
Stroke="Black"
|
||||
StrokeThickness="1"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}"/>
|
||||
</Grid>
|
||||
<Ellipse
|
||||
Fill="Red"
|
||||
Width="{Binding Visualizer.UiStickCircumference}"
|
||||
Height="{Binding Visualizer.UiStickCircumference}"
|
||||
Canvas.Bottom="{Binding Visualizer.UiStickLeftY}"
|
||||
Canvas.Left="{Binding Visualizer.UiStickLeftX}" />
|
||||
</Canvas>
|
||||
</Border>
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Height="{Binding Visualizer.UiStickBorderSize}"
|
||||
Width="{Binding Visualizer.UiStickBorderSize}"
|
||||
IsVisible="{Binding IsRight}">
|
||||
<Canvas
|
||||
Background="{DynamicResource ThemeBackgroundColor}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}">
|
||||
<Grid
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Background="{DynamicResource ThemeBackgroundColor}">
|
||||
<Ellipse
|
||||
HorizontalAlignment="Center"
|
||||
Stroke="Black"
|
||||
StrokeThickness="1"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}"/>
|
||||
</Grid>
|
||||
<Ellipse
|
||||
Fill="Red"
|
||||
Width="{Binding Visualizer.UiStickCircumference}"
|
||||
Height="{Binding Visualizer.UiStickCircumference}"
|
||||
Canvas.Bottom="{Binding Visualizer.UiStickRightY}"
|
||||
Canvas.Left="{Binding Visualizer.UiStickRightX}" />
|
||||
</Canvas>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
xmlns:settings="clr-namespace:Ryujinx.Ava.UI.Views.Settings"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
Width="1100"
|
||||
Height="768"
|
||||
Height="850"
|
||||
MinWidth="800"
|
||||
MinHeight="480"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
|
||||
Reference in New Issue
Block a user