Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4ce4299ca2 | ||
|
17620d18db | ||
|
9f1cf6458c | ||
|
67b4e63cff | ||
|
c05c688ee8 | ||
|
b2623dc27d | ||
|
5131b71437 | ||
|
7870423671 | ||
|
b72916fbc1 | ||
|
da073fce61 | ||
|
1fc90e57d2 | ||
|
eafcc314a9 | ||
|
6e9bd4de13 | ||
|
05a41b31bc | ||
|
eed17f963e | ||
|
c09c0c002d | ||
|
d56d335c0b | ||
|
23c844b2aa |
@@ -63,6 +63,10 @@ dotnet_code_quality_unused_parameters = all:suggestion
|
||||
|
||||
#### C# Coding Conventions ####
|
||||
|
||||
# Namespace preferences
|
||||
csharp_style_namespace_declarations = block_scoped:warning
|
||||
resharper_csharp_namespace_body = block_scoped
|
||||
|
||||
# var preferences
|
||||
csharp_style_var_elsewhere = false:silent
|
||||
csharp_style_var_for_built_in_types = false:silent
|
||||
|
14
.github/workflows/flatpak.yml
vendored
14
.github/workflows/flatpak.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
id: version_info
|
||||
working-directory: Ryujinx
|
||||
run: |
|
||||
echo "git_short_hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "git_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
- name: Update flatpak metadata
|
||||
id: metadata
|
||||
env:
|
||||
RYUJINX_GIT_HASH: ${{ steps.version_info.outputs.git_short_hash }}
|
||||
RYUJINX_GIT_HASH: ${{ steps.version_info.outputs.git_hash }}
|
||||
shell: python
|
||||
run: |
|
||||
import hashlib
|
||||
@@ -95,6 +95,16 @@ jobs:
|
||||
from datetime import datetime
|
||||
from lxml import etree
|
||||
|
||||
|
||||
# Ensure we don't destroy multiline strings
|
||||
def str_presenter(dumper, data):
|
||||
if len(data.splitlines()) > 1:
|
||||
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
|
||||
return dumper.represent_scalar("tag:yaml.org,2002:str", data)
|
||||
|
||||
|
||||
yaml.representer.SafeRepresenter.add_representer(str, str_presenter)
|
||||
|
||||
yaml_file = "flathub/org.ryujinx.Ryujinx.yml"
|
||||
xml_file = "flathub/org.ryujinx.Ryujinx.appdata.xml"
|
||||
|
||||
|
@@ -7,6 +7,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Memory\Ryujinx.Memory.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
using ARMeilleure.CodeGen.Linking;
|
||||
using ARMeilleure.CodeGen.RegisterAllocators;
|
||||
using ARMeilleure.IntermediateRepresentation;
|
||||
using Ryujinx.Common.Memory;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -59,7 +60,7 @@ namespace ARMeilleure.CodeGen.Arm64
|
||||
|
||||
public CodeGenContext(AllocationResult allocResult, int maxCallArgs, int blocksCount, bool relocatable)
|
||||
{
|
||||
_stream = new MemoryStream();
|
||||
_stream = MemoryStreamManager.Shared.GetStream();
|
||||
|
||||
AllocResult = allocResult;
|
||||
|
||||
@@ -265,7 +266,7 @@ namespace ARMeilleure.CodeGen.Arm64
|
||||
}
|
||||
else
|
||||
{
|
||||
relocInfo = new RelocInfo(new RelocEntry[0]);
|
||||
relocInfo = new RelocInfo(Array.Empty<RelocEntry>());
|
||||
}
|
||||
|
||||
return (code, relocInfo);
|
||||
|
@@ -433,16 +433,11 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
|
||||
|
||||
private static int GetHighestValueIndex(Span<int> span)
|
||||
{
|
||||
int highest = span[0];
|
||||
|
||||
if (highest == int.MaxValue)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
int highest = int.MinValue;
|
||||
|
||||
int selected = 0;
|
||||
|
||||
for (int index = 1; index < span.Length; index++)
|
||||
for (int index = 0; index < span.Length; index++)
|
||||
{
|
||||
int current = span[index];
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
using ARMeilleure.CodeGen.Linking;
|
||||
using ARMeilleure.IntermediateRepresentation;
|
||||
using Ryujinx.Common.Memory;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@@ -1033,7 +1034,13 @@ namespace ARMeilleure.CodeGen.X86
|
||||
|
||||
Debug.Assert(opCode != BadOp, "Invalid opcode value.");
|
||||
|
||||
if ((flags & InstructionFlags.Vex) != 0 && HardwareCapabilities.SupportsVexEncoding)
|
||||
if ((flags & InstructionFlags.Evex) != 0 && HardwareCapabilities.SupportsEvexEncoding)
|
||||
{
|
||||
WriteEvexInst(dest, src1, src2, type, flags, opCode);
|
||||
|
||||
opCode &= 0xff;
|
||||
}
|
||||
else if ((flags & InstructionFlags.Vex) != 0 && HardwareCapabilities.SupportsVexEncoding)
|
||||
{
|
||||
// In a vex encoding, only one prefix can be active at a time. The active prefix is encoded in the second byte using two bits.
|
||||
|
||||
@@ -1152,6 +1159,103 @@ namespace ARMeilleure.CodeGen.X86
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteEvexInst(
|
||||
Operand dest,
|
||||
Operand src1,
|
||||
Operand src2,
|
||||
OperandType type,
|
||||
InstructionFlags flags,
|
||||
int opCode,
|
||||
bool broadcast = false,
|
||||
int registerWidth = 128,
|
||||
int maskRegisterIdx = 0,
|
||||
bool zeroElements = false)
|
||||
{
|
||||
int op1Idx = dest.GetRegister().Index;
|
||||
int op2Idx = src1.GetRegister().Index;
|
||||
int op3Idx = src2.GetRegister().Index;
|
||||
|
||||
WriteByte(0x62);
|
||||
|
||||
// P0
|
||||
// Extend operand 1 register
|
||||
bool r = (op1Idx & 8) == 0;
|
||||
// Extend operand 3 register
|
||||
bool x = (op3Idx & 16) == 0;
|
||||
// Extend operand 3 register
|
||||
bool b = (op3Idx & 8) == 0;
|
||||
// Extend operand 1 register
|
||||
bool rp = (op1Idx & 16) == 0;
|
||||
// Escape code index
|
||||
byte mm = 0b00;
|
||||
|
||||
switch ((ushort)(opCode >> 8))
|
||||
{
|
||||
case 0xf00: mm = 0b01; break;
|
||||
case 0xf38: mm = 0b10; break;
|
||||
case 0xf3a: mm = 0b11; break;
|
||||
|
||||
default: Debug.Fail($"Failed to EVEX encode opcode 0x{opCode:X}."); break;
|
||||
}
|
||||
|
||||
WriteByte(
|
||||
(byte)(
|
||||
(r ? 0x80 : 0) |
|
||||
(x ? 0x40 : 0) |
|
||||
(b ? 0x20 : 0) |
|
||||
(rp ? 0x10 : 0) |
|
||||
mm));
|
||||
|
||||
// P1
|
||||
// Specify 64-bit lane mode
|
||||
bool w = Is64Bits(type);
|
||||
// Operand 2 register index
|
||||
byte vvvv = (byte)(~op2Idx & 0b1111);
|
||||
// Opcode prefix
|
||||
byte pp = (flags & InstructionFlags.PrefixMask) switch
|
||||
{
|
||||
InstructionFlags.Prefix66 => 0b01,
|
||||
InstructionFlags.PrefixF3 => 0b10,
|
||||
InstructionFlags.PrefixF2 => 0b11,
|
||||
_ => 0
|
||||
};
|
||||
WriteByte(
|
||||
(byte)(
|
||||
(w ? 0x80 : 0) |
|
||||
(vvvv << 3) |
|
||||
0b100 |
|
||||
pp));
|
||||
|
||||
// P2
|
||||
// Mask register determines what elements to zero, rather than what elements to merge
|
||||
bool z = zeroElements;
|
||||
// Specifies register-width
|
||||
byte ll = 0b00;
|
||||
switch (registerWidth)
|
||||
{
|
||||
case 128: ll = 0b00; break;
|
||||
case 256: ll = 0b01; break;
|
||||
case 512: ll = 0b10; break;
|
||||
|
||||
default: Debug.Fail($"Invalid EVEX vector register width {registerWidth}."); break;
|
||||
}
|
||||
// Embedded broadcast in the case of a memory operand
|
||||
bool bcast = broadcast;
|
||||
// Extend operand 2 register
|
||||
bool vp = (op2Idx & 16) == 0;
|
||||
// Mask register index
|
||||
Debug.Assert(maskRegisterIdx < 8, $"Invalid mask register index {maskRegisterIdx}.");
|
||||
byte aaa = (byte)(maskRegisterIdx & 0b111);
|
||||
|
||||
WriteByte(
|
||||
(byte)(
|
||||
(z ? 0x80 : 0) |
|
||||
(ll << 5) |
|
||||
(bcast ? 0x10 : 0) |
|
||||
(vp ? 8 : 0) |
|
||||
aaa));
|
||||
}
|
||||
|
||||
private void WriteCompactInst(Operand operand, int opCode)
|
||||
{
|
||||
int regIndex = operand.GetRegister().Index;
|
||||
@@ -1285,7 +1389,7 @@ namespace ARMeilleure.CodeGen.X86
|
||||
// Write the code, ignoring the dummy bytes after jumps, into a new stream.
|
||||
_stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
using var codeStream = new MemoryStream();
|
||||
using var codeStream = MemoryStreamManager.Shared.GetStream();
|
||||
var assembler = new Assembler(codeStream, HasRelocs);
|
||||
|
||||
bool hasRelocs = HasRelocs;
|
||||
|
@@ -20,6 +20,7 @@ namespace ARMeilleure.CodeGen.X86
|
||||
Reg8Dest = 1 << 2,
|
||||
RexW = 1 << 3,
|
||||
Vex = 1 << 4,
|
||||
Evex = 1 << 5,
|
||||
|
||||
PrefixBit = 16,
|
||||
PrefixMask = 7 << PrefixBit,
|
||||
@@ -278,6 +279,7 @@ namespace ARMeilleure.CodeGen.X86
|
||||
Add(X86Instruction.Vfnmsub231sd, new InstructionInfo(BadOp, BadOp, BadOp, BadOp, 0x000f38bf, InstructionFlags.Vex | InstructionFlags.Prefix66 | InstructionFlags.RexW));
|
||||
Add(X86Instruction.Vfnmsub231ss, new InstructionInfo(BadOp, BadOp, BadOp, BadOp, 0x000f38bf, InstructionFlags.Vex | InstructionFlags.Prefix66));
|
||||
Add(X86Instruction.Vpblendvb, new InstructionInfo(BadOp, BadOp, BadOp, BadOp, 0x000f3a4c, InstructionFlags.Vex | InstructionFlags.Prefix66));
|
||||
Add(X86Instruction.Vpternlogd, new InstructionInfo(BadOp, BadOp, BadOp, BadOp, 0x000f3a25, InstructionFlags.Evex | InstructionFlags.Prefix66));
|
||||
Add(X86Instruction.Xor, new InstructionInfo(0x00000031, 0x06000083, 0x06000081, BadOp, 0x00000033, InstructionFlags.None));
|
||||
Add(X86Instruction.Xorpd, new InstructionInfo(BadOp, BadOp, BadOp, BadOp, 0x00000f57, InstructionFlags.Vex | InstructionFlags.Prefix66));
|
||||
Add(X86Instruction.Xorps, new InstructionInfo(BadOp, BadOp, BadOp, BadOp, 0x00000f57, InstructionFlags.Vex));
|
||||
|
@@ -1,5 +1,6 @@
|
||||
using ARMeilleure.CodeGen.RegisterAllocators;
|
||||
using ARMeilleure.IntermediateRepresentation;
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
|
||||
@@ -22,7 +23,7 @@ namespace ARMeilleure.CodeGen.X86
|
||||
|
||||
public CodeGenContext(AllocationResult allocResult, int maxCallArgs, int blocksCount, bool relocatable)
|
||||
{
|
||||
_stream = new MemoryStream();
|
||||
_stream = MemoryStreamManager.Shared.GetStream();
|
||||
_blockLabels = new Operand[blocksCount];
|
||||
|
||||
AllocResult = allocResult;
|
||||
|
@@ -1,10 +1,14 @@
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
namespace ARMeilleure.CodeGen.X86
|
||||
{
|
||||
static class HardwareCapabilities
|
||||
{
|
||||
private delegate uint GetXcr0();
|
||||
|
||||
static HardwareCapabilities()
|
||||
{
|
||||
if (!X86Base.IsSupported)
|
||||
@@ -24,6 +28,28 @@ namespace ARMeilleure.CodeGen.X86
|
||||
FeatureInfo7Ebx = (FeatureFlags7Ebx)ebx7;
|
||||
FeatureInfo7Ecx = (FeatureFlags7Ecx)ecx7;
|
||||
}
|
||||
|
||||
Xcr0InfoEax = (Xcr0FlagsEax)GetXcr0Eax();
|
||||
}
|
||||
|
||||
private static uint GetXcr0Eax()
|
||||
{
|
||||
ReadOnlySpan<byte> asmGetXcr0 = new byte[]
|
||||
{
|
||||
0x31, 0xc9, // xor ecx, ecx
|
||||
0xf, 0x01, 0xd0, // xgetbv
|
||||
0xc3, // ret
|
||||
};
|
||||
|
||||
using MemoryBlock memGetXcr0 = new MemoryBlock((ulong)asmGetXcr0.Length);
|
||||
|
||||
memGetXcr0.Write(0, asmGetXcr0);
|
||||
|
||||
memGetXcr0.Reprotect(0, (ulong)asmGetXcr0.Length, MemoryPermission.ReadAndExecute);
|
||||
|
||||
var fGetXcr0 = Marshal.GetDelegateForFunctionPointer<GetXcr0>(memGetXcr0.Pointer);
|
||||
|
||||
return fGetXcr0();
|
||||
}
|
||||
|
||||
[Flags]
|
||||
@@ -44,6 +70,7 @@ namespace ARMeilleure.CodeGen.X86
|
||||
Sse42 = 1 << 20,
|
||||
Popcnt = 1 << 23,
|
||||
Aes = 1 << 25,
|
||||
Osxsave = 1 << 27,
|
||||
Avx = 1 << 28,
|
||||
F16c = 1 << 29
|
||||
}
|
||||
@@ -52,7 +79,11 @@ namespace ARMeilleure.CodeGen.X86
|
||||
public enum FeatureFlags7Ebx
|
||||
{
|
||||
Avx2 = 1 << 5,
|
||||
Sha = 1 << 29
|
||||
Avx512f = 1 << 16,
|
||||
Avx512dq = 1 << 17,
|
||||
Sha = 1 << 29,
|
||||
Avx512bw = 1 << 30,
|
||||
Avx512vl = 1 << 31
|
||||
}
|
||||
|
||||
[Flags]
|
||||
@@ -61,10 +92,21 @@ namespace ARMeilleure.CodeGen.X86
|
||||
Gfni = 1 << 8,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum Xcr0FlagsEax
|
||||
{
|
||||
Sse = 1 << 1,
|
||||
YmmHi128 = 1 << 2,
|
||||
Opmask = 1 << 5,
|
||||
ZmmHi256 = 1 << 6,
|
||||
Hi16Zmm = 1 << 7
|
||||
}
|
||||
|
||||
public static FeatureFlags1Edx FeatureInfo1Edx { get; }
|
||||
public static FeatureFlags1Ecx FeatureInfo1Ecx { get; }
|
||||
public static FeatureFlags7Ebx FeatureInfo7Ebx { get; } = 0;
|
||||
public static FeatureFlags7Ecx FeatureInfo7Ecx { get; } = 0;
|
||||
public static Xcr0FlagsEax Xcr0InfoEax { get; } = 0;
|
||||
|
||||
public static bool SupportsSse => FeatureInfo1Edx.HasFlag(FeatureFlags1Edx.Sse);
|
||||
public static bool SupportsSse2 => FeatureInfo1Edx.HasFlag(FeatureFlags1Edx.Sse2);
|
||||
@@ -76,8 +118,13 @@ namespace ARMeilleure.CodeGen.X86
|
||||
public static bool SupportsSse42 => FeatureInfo1Ecx.HasFlag(FeatureFlags1Ecx.Sse42);
|
||||
public static bool SupportsPopcnt => FeatureInfo1Ecx.HasFlag(FeatureFlags1Ecx.Popcnt);
|
||||
public static bool SupportsAesni => FeatureInfo1Ecx.HasFlag(FeatureFlags1Ecx.Aes);
|
||||
public static bool SupportsAvx => FeatureInfo1Ecx.HasFlag(FeatureFlags1Ecx.Avx);
|
||||
public static bool SupportsAvx => FeatureInfo1Ecx.HasFlag(FeatureFlags1Ecx.Avx | FeatureFlags1Ecx.Osxsave) && Xcr0InfoEax.HasFlag(Xcr0FlagsEax.Sse | Xcr0FlagsEax.YmmHi128);
|
||||
public static bool SupportsAvx2 => FeatureInfo7Ebx.HasFlag(FeatureFlags7Ebx.Avx2) && SupportsAvx;
|
||||
public static bool SupportsAvx512F => FeatureInfo7Ebx.HasFlag(FeatureFlags7Ebx.Avx512f) && FeatureInfo1Ecx.HasFlag(FeatureFlags1Ecx.Osxsave)
|
||||
&& Xcr0InfoEax.HasFlag(Xcr0FlagsEax.Sse | Xcr0FlagsEax.YmmHi128 | Xcr0FlagsEax.Opmask | Xcr0FlagsEax.ZmmHi256 | Xcr0FlagsEax.Hi16Zmm);
|
||||
public static bool SupportsAvx512Vl => FeatureInfo7Ebx.HasFlag(FeatureFlags7Ebx.Avx512vl) && SupportsAvx512F;
|
||||
public static bool SupportsAvx512Bw => FeatureInfo7Ebx.HasFlag(FeatureFlags7Ebx.Avx512bw) && SupportsAvx512F;
|
||||
public static bool SupportsAvx512Dq => FeatureInfo7Ebx.HasFlag(FeatureFlags7Ebx.Avx512dq) && SupportsAvx512F;
|
||||
public static bool SupportsF16c => FeatureInfo1Ecx.HasFlag(FeatureFlags1Ecx.F16c);
|
||||
public static bool SupportsSha => FeatureInfo7Ebx.HasFlag(FeatureFlags7Ebx.Sha);
|
||||
public static bool SupportsGfni => FeatureInfo7Ecx.HasFlag(FeatureFlags7Ecx.Gfni);
|
||||
@@ -85,5 +132,6 @@ namespace ARMeilleure.CodeGen.X86
|
||||
public static bool ForceLegacySse { get; set; }
|
||||
|
||||
public static bool SupportsVexEncoding => SupportsAvx && !ForceLegacySse;
|
||||
public static bool SupportsEvexEncoding => SupportsAvx512F && !ForceLegacySse;
|
||||
}
|
||||
}
|
@@ -180,6 +180,7 @@ namespace ARMeilleure.CodeGen.X86
|
||||
Add(Intrinsic.X86Vfnmadd231ss, new IntrinsicInfo(X86Instruction.Vfnmadd231ss, IntrinsicType.Fma));
|
||||
Add(Intrinsic.X86Vfnmsub231sd, new IntrinsicInfo(X86Instruction.Vfnmsub231sd, IntrinsicType.Fma));
|
||||
Add(Intrinsic.X86Vfnmsub231ss, new IntrinsicInfo(X86Instruction.Vfnmsub231ss, IntrinsicType.Fma));
|
||||
Add(Intrinsic.X86Vpternlogd, new IntrinsicInfo(X86Instruction.Vpternlogd, IntrinsicType.TernaryImm));
|
||||
Add(Intrinsic.X86Xorpd, new IntrinsicInfo(X86Instruction.Xorpd, IntrinsicType.Binary));
|
||||
Add(Intrinsic.X86Xorps, new IntrinsicInfo(X86Instruction.Xorps, IntrinsicType.Binary));
|
||||
}
|
||||
|
@@ -219,6 +219,7 @@ namespace ARMeilleure.CodeGen.X86
|
||||
Vfnmsub231sd,
|
||||
Vfnmsub231ss,
|
||||
Vpblendvb,
|
||||
Vpternlogd,
|
||||
Xor,
|
||||
Xorpd,
|
||||
Xorps,
|
||||
|
@@ -17,7 +17,7 @@ namespace ARMeilleure.Decoders
|
||||
{
|
||||
uint[] tbl = new uint[256];
|
||||
|
||||
for (int idx = 0; idx < 256; idx++)
|
||||
for (int idx = 0; idx < tbl.Length; idx++)
|
||||
{
|
||||
tbl[idx] = ExpandImm8ToFP32((uint)idx);
|
||||
}
|
||||
@@ -29,7 +29,7 @@ namespace ARMeilleure.Decoders
|
||||
{
|
||||
ulong[] tbl = new ulong[256];
|
||||
|
||||
for (int idx = 0; idx < 256; idx++)
|
||||
for (int idx = 0; idx < tbl.Length; idx++)
|
||||
{
|
||||
tbl[idx] = ExpandImm8ToFP64((ulong)idx);
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
namespace ARMeilleure.Decoders;
|
||||
|
||||
interface IOpCode32Exception
|
||||
namespace ARMeilleure.Decoders
|
||||
{
|
||||
interface IOpCode32Exception
|
||||
{
|
||||
int Id { get; }
|
||||
}
|
||||
}
|
@@ -1301,7 +1301,7 @@ namespace ARMeilleure.Decoders
|
||||
{
|
||||
List<InstInfo>[] temp = new List<InstInfo>[FastLookupSize];
|
||||
|
||||
for (int index = 0; index < FastLookupSize; index++)
|
||||
for (int index = 0; index < temp.Length; index++)
|
||||
{
|
||||
temp[index] = new List<InstInfo>();
|
||||
}
|
||||
@@ -1311,7 +1311,7 @@ namespace ARMeilleure.Decoders
|
||||
int mask = ToFastLookupIndex(inst.Mask);
|
||||
int value = ToFastLookupIndex(inst.Value);
|
||||
|
||||
for (int index = 0; index < FastLookupSize; index++)
|
||||
for (int index = 0; index < temp.Length; index++)
|
||||
{
|
||||
if ((index & mask) == value)
|
||||
{
|
||||
@@ -1320,7 +1320,7 @@ namespace ARMeilleure.Decoders
|
||||
}
|
||||
}
|
||||
|
||||
for (int index = 0; index < FastLookupSize; index++)
|
||||
for (int index = 0; index < temp.Length; index++)
|
||||
{
|
||||
table[index] = temp[index].ToArray();
|
||||
}
|
||||
|
@@ -254,7 +254,22 @@ namespace ARMeilleure.Instructions
|
||||
|
||||
public static void Not_V(ArmEmitterContext context)
|
||||
{
|
||||
if (Optimizations.UseSse2)
|
||||
if (Optimizations.UseAvx512Ortho)
|
||||
{
|
||||
OpCodeSimd op = (OpCodeSimd)context.CurrOp;
|
||||
|
||||
Operand n = GetVec(op.Rn);
|
||||
|
||||
Operand res = context.AddIntrinsic(Intrinsic.X86Vpternlogd, n, n, Const(~0b10101010));
|
||||
|
||||
if (op.RegisterSize == RegisterSize.Simd64)
|
||||
{
|
||||
res = context.VectorZeroUpper64(res);
|
||||
}
|
||||
|
||||
context.Copy(GetVec(op.Rd), res);
|
||||
}
|
||||
else if (Optimizations.UseSse2)
|
||||
{
|
||||
OpCodeSimd op = (OpCodeSimd)context.CurrOp;
|
||||
|
||||
@@ -283,6 +298,22 @@ namespace ARMeilleure.Instructions
|
||||
{
|
||||
InstEmitSimdHelperArm64.EmitVectorBinaryOp(context, Intrinsic.Arm64OrnV);
|
||||
}
|
||||
else if (Optimizations.UseAvx512Ortho)
|
||||
{
|
||||
OpCodeSimdReg op = (OpCodeSimdReg)context.CurrOp;
|
||||
|
||||
Operand n = GetVec(op.Rn);
|
||||
Operand m = GetVec(op.Rm);
|
||||
|
||||
Operand res = context.AddIntrinsic(Intrinsic.X86Vpternlogd, n, m, Const(0b11001100 | ~0b10101010));
|
||||
|
||||
if (op.RegisterSize == RegisterSize.Simd64)
|
||||
{
|
||||
res = context.VectorZeroUpper64(res);
|
||||
}
|
||||
|
||||
context.Copy(GetVec(op.Rd), res);
|
||||
}
|
||||
else if (Optimizations.UseSse2)
|
||||
{
|
||||
OpCodeSimdReg op = (OpCodeSimdReg)context.CurrOp;
|
||||
|
@@ -151,6 +151,13 @@ namespace ARMeilleure.Instructions
|
||||
{
|
||||
InstEmitSimdHelper32Arm64.EmitVectorBinaryOpSimd32(context, (n, m) => context.AddIntrinsic(Intrinsic.Arm64OrnV | Intrinsic.Arm64V128, n, m));
|
||||
}
|
||||
else if (Optimizations.UseAvx512Ortho)
|
||||
{
|
||||
EmitVectorBinaryOpSimd32(context, (n, m) =>
|
||||
{
|
||||
return context.AddIntrinsic(Intrinsic.X86Vpternlogd, n, m, Const(0b11001100 | ~0b10101010));
|
||||
});
|
||||
}
|
||||
else if (Optimizations.UseSse2)
|
||||
{
|
||||
Operand mask = context.VectorOne();
|
||||
|
@@ -34,7 +34,14 @@ namespace ARMeilleure.Instructions
|
||||
|
||||
public static void Vmvn_I(ArmEmitterContext context)
|
||||
{
|
||||
if (Optimizations.UseSse2)
|
||||
if (Optimizations.UseAvx512Ortho)
|
||||
{
|
||||
EmitVectorUnaryOpSimd32(context, (op1) =>
|
||||
{
|
||||
return context.AddIntrinsic(Intrinsic.X86Vpternlogd, op1, op1, Const(0b01010101));
|
||||
});
|
||||
}
|
||||
else if (Optimizations.UseSse2)
|
||||
{
|
||||
EmitVectorUnaryOpSimd32(context, (op1) =>
|
||||
{
|
||||
|
@@ -173,6 +173,7 @@ namespace ARMeilleure.IntermediateRepresentation
|
||||
X86Vfnmadd231ss,
|
||||
X86Vfnmsub231sd,
|
||||
X86Vfnmsub231ss,
|
||||
X86Vpternlogd,
|
||||
X86Xorpd,
|
||||
X86Xorps,
|
||||
|
||||
|
@@ -23,6 +23,10 @@ namespace ARMeilleure
|
||||
public static bool UseSse42IfAvailable { get; set; } = true;
|
||||
public static bool UsePopCntIfAvailable { get; set; } = true;
|
||||
public static bool UseAvxIfAvailable { get; set; } = true;
|
||||
public static bool UseAvx512FIfAvailable { get; set; } = true;
|
||||
public static bool UseAvx512VlIfAvailable { get; set; } = true;
|
||||
public static bool UseAvx512BwIfAvailable { get; set; } = true;
|
||||
public static bool UseAvx512DqIfAvailable { get; set; } = true;
|
||||
public static bool UseF16cIfAvailable { get; set; } = true;
|
||||
public static bool UseFmaIfAvailable { get; set; } = true;
|
||||
public static bool UseAesniIfAvailable { get; set; } = true;
|
||||
@@ -47,11 +51,18 @@ namespace ARMeilleure
|
||||
internal static bool UseSse42 => UseSse42IfAvailable && X86HardwareCapabilities.SupportsSse42;
|
||||
internal static bool UsePopCnt => UsePopCntIfAvailable && X86HardwareCapabilities.SupportsPopcnt;
|
||||
internal static bool UseAvx => UseAvxIfAvailable && X86HardwareCapabilities.SupportsAvx && !ForceLegacySse;
|
||||
internal static bool UseAvx512F => UseAvx512FIfAvailable && X86HardwareCapabilities.SupportsAvx512F && !ForceLegacySse;
|
||||
internal static bool UseAvx512Vl => UseAvx512VlIfAvailable && X86HardwareCapabilities.SupportsAvx512Vl && !ForceLegacySse;
|
||||
internal static bool UseAvx512Bw => UseAvx512BwIfAvailable && X86HardwareCapabilities.SupportsAvx512Bw && !ForceLegacySse;
|
||||
internal static bool UseAvx512Dq => UseAvx512DqIfAvailable && X86HardwareCapabilities.SupportsAvx512Dq && !ForceLegacySse;
|
||||
internal static bool UseF16c => UseF16cIfAvailable && X86HardwareCapabilities.SupportsF16c;
|
||||
internal static bool UseFma => UseFmaIfAvailable && X86HardwareCapabilities.SupportsFma;
|
||||
internal static bool UseAesni => UseAesniIfAvailable && X86HardwareCapabilities.SupportsAesni;
|
||||
internal static bool UsePclmulqdq => UsePclmulqdqIfAvailable && X86HardwareCapabilities.SupportsPclmulqdq;
|
||||
internal static bool UseSha => UseShaIfAvailable && X86HardwareCapabilities.SupportsSha;
|
||||
internal static bool UseGfni => UseGfniIfAvailable && X86HardwareCapabilities.SupportsGfni;
|
||||
|
||||
internal static bool UseAvx512Ortho => UseAvx512F && UseAvx512Vl;
|
||||
internal static bool UseAvx512OrthoFloat => UseAvx512Ortho && UseAvx512Dq;
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ using ARMeilleure.Memory;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Memory;
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
@@ -29,7 +30,7 @@ namespace ARMeilleure.Translation.PTC
|
||||
private const string OuterHeaderMagicString = "PTCohd\0\0";
|
||||
private const string InnerHeaderMagicString = "PTCihd\0\0";
|
||||
|
||||
private const uint InternalVersion = 4484; //! To be incremented manually for each change to the ARMeilleure project.
|
||||
private const uint InternalVersion = 4485; //! To be incremented manually for each change to the ARMeilleure project.
|
||||
|
||||
private const string ActualDir = "0";
|
||||
private const string BackupDir = "1";
|
||||
@@ -150,10 +151,10 @@ namespace ARMeilleure.Translation.PTC
|
||||
|
||||
private void InitializeCarriers()
|
||||
{
|
||||
_infosStream = new MemoryStream();
|
||||
_infosStream = MemoryStreamManager.Shared.GetStream();
|
||||
_codesList = new List<byte[]>();
|
||||
_relocsStream = new MemoryStream();
|
||||
_unwindInfosStream = new MemoryStream();
|
||||
_relocsStream = MemoryStreamManager.Shared.GetStream();
|
||||
_unwindInfosStream = MemoryStreamManager.Shared.GetStream();
|
||||
}
|
||||
|
||||
private void DisposeCarriers()
|
||||
@@ -968,6 +969,7 @@ namespace ARMeilleure.Translation.PTC
|
||||
(ulong)Arm64HardwareCapabilities.LinuxFeatureInfoHwCap,
|
||||
(ulong)Arm64HardwareCapabilities.LinuxFeatureInfoHwCap2,
|
||||
(ulong)Arm64HardwareCapabilities.MacOsFeatureInfo,
|
||||
0,
|
||||
0);
|
||||
}
|
||||
else if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
||||
@@ -976,11 +978,12 @@ namespace ARMeilleure.Translation.PTC
|
||||
(ulong)X86HardwareCapabilities.FeatureInfo1Ecx,
|
||||
(ulong)X86HardwareCapabilities.FeatureInfo1Edx,
|
||||
(ulong)X86HardwareCapabilities.FeatureInfo7Ebx,
|
||||
(ulong)X86HardwareCapabilities.FeatureInfo7Ecx);
|
||||
(ulong)X86HardwareCapabilities.FeatureInfo7Ecx,
|
||||
(ulong)X86HardwareCapabilities.Xcr0InfoEax);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new FeatureInfo(0, 0, 0, 0);
|
||||
return new FeatureInfo(0, 0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1001,7 +1004,7 @@ namespace ARMeilleure.Translation.PTC
|
||||
return osPlatform;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 78*/)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 86*/)]
|
||||
private struct OuterHeader
|
||||
{
|
||||
public ulong Magic;
|
||||
@@ -1033,8 +1036,8 @@ namespace ARMeilleure.Translation.PTC
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 32*/)]
|
||||
private record struct FeatureInfo(ulong FeatureInfo0, ulong FeatureInfo1, ulong FeatureInfo2, ulong FeatureInfo3);
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 40*/)]
|
||||
private record struct FeatureInfo(ulong FeatureInfo0, ulong FeatureInfo1, ulong FeatureInfo2, ulong FeatureInfo3, ulong FeatureInfo4);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 128*/)]
|
||||
private struct InnerHeader
|
||||
|
@@ -1,6 +1,7 @@
|
||||
using ARMeilleure.State;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Memory;
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
@@ -182,7 +183,7 @@ namespace ARMeilleure.Translation.PTC
|
||||
return false;
|
||||
}
|
||||
|
||||
using (MemoryStream stream = new MemoryStream())
|
||||
using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
|
||||
{
|
||||
Debug.Assert(stream.Seek(0L, SeekOrigin.Begin) == 0L && stream.Length == 0L);
|
||||
|
||||
@@ -274,7 +275,7 @@ namespace ARMeilleure.Translation.PTC
|
||||
|
||||
outerHeader.SetHeaderHash();
|
||||
|
||||
using (MemoryStream stream = new MemoryStream())
|
||||
using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
|
||||
{
|
||||
Debug.Assert(stream.Seek(0L, SeekOrigin.Begin) == 0L && stream.Length == 0L);
|
||||
|
||||
|
@@ -12,7 +12,6 @@
|
||||
<PackageVersion Include="Avalonia.Svg.Skia" Version="0.10.18" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="Concentus" Version="1.1.7" />
|
||||
<PackageVersion Include="Crc32.NET" Version="1.2.0" />
|
||||
<PackageVersion Include="DiscordRichPresence" Version="1.1.3.18" />
|
||||
<PackageVersion Include="DynamicData" Version="7.12.11" />
|
||||
<PackageVersion Include="FluentAvaloniaUI" Version="1.4.5" />
|
||||
@@ -23,6 +22,7 @@
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
|
||||
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
|
||||
<PackageVersion Include="NUnit" Version="3.13.3" />
|
||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
@@ -45,11 +45,9 @@
|
||||
<PackageVersion Include="SPB" Version="0.0.4-build28" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="7.0.0" />
|
||||
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="6.27.0" />
|
||||
<PackageVersion Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
|
||||
<PackageVersion Include="System.IO.Hashing" Version="7.0.0" />
|
||||
<PackageVersion Include="System.Management" Version="7.0.0" />
|
||||
<PackageVersion Include="System.Net.NameResolution" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Threading.ThreadPool" Version="4.3.0" />
|
||||
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-f7c841d" />
|
||||
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
|
||||
<PackageVersion Include="XamlNameReferenceGenerator" Version="1.5.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@@ -400,7 +400,9 @@ namespace Ryujinx.Audio.Common
|
||||
{
|
||||
uint bufferIndex = (_releasedBufferIndex - _bufferReleasedCount) % Constants.AudioDeviceBufferCountMax;
|
||||
|
||||
for (int i = 0; i < GetTotalBufferCount(); i++)
|
||||
uint totalBufferCount = GetTotalBufferCount();
|
||||
|
||||
for (int i = 0; i < totalBufferCount; i++)
|
||||
{
|
||||
if (_buffers[bufferIndex].BufferTag == bufferTag)
|
||||
{
|
||||
|
@@ -583,10 +583,10 @@
|
||||
"SelectUpdateDialogTitle": "Select update files",
|
||||
"UserProfileWindowTitle": "User Profiles Manager",
|
||||
"CheatWindowTitle": "Cheats Manager",
|
||||
"DlcWindowTitle": "Downloadable Content Manager",
|
||||
"DlcWindowTitle": "Manage Downloadable Content for {0} ({1})",
|
||||
"UpdateWindowTitle": "Title Update Manager",
|
||||
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
|
||||
"DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})",
|
||||
"DlcWindowHeading": "{0} Downloadable Content(s)",
|
||||
"UserProfilesEditProfile": "Edit Selected",
|
||||
"Cancel": "Cancel",
|
||||
"Save": "Save",
|
||||
|
@@ -130,7 +130,7 @@ namespace Ryujinx.Ava.Common.Locale
|
||||
{
|
||||
var localeStrings = new Dictionary<LocaleKeys, string>();
|
||||
string languageJson = EmbeddedResources.ReadAllText($"Ryujinx.Ava/Assets/Locales/{languageCode}.json");
|
||||
var strings = JsonHelper.Deserialize<Dictionary<string, string>>(languageJson);
|
||||
var strings = JsonHelper.Deserialize(languageJson, CommonJsonContext.Default.StringDictionary);
|
||||
|
||||
foreach (var item in strings)
|
||||
{
|
||||
|
@@ -4,13 +4,14 @@ using FluentAvalonia.UI.Controls;
|
||||
using ICSharpCode.SharpZipLib.GZip;
|
||||
using ICSharpCode.SharpZipLib.Tar;
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Ryujinx.Ava;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using Ryujinx.Ui.Common.Models.Github;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@@ -31,6 +32,7 @@ namespace Ryujinx.Modules
|
||||
internal static class Updater
|
||||
{
|
||||
private const string GitHubApiURL = "https://api.github.com";
|
||||
private static readonly GithubReleasesJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
private static readonly string HomeDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||
private static readonly string UpdateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
|
||||
@@ -99,22 +101,16 @@ namespace Ryujinx.Modules
|
||||
|
||||
string buildInfoURL = $"{GitHubApiURL}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
|
||||
string fetchedJson = await jsonClient.GetStringAsync(buildInfoURL);
|
||||
JObject jsonRoot = JObject.Parse(fetchedJson);
|
||||
JToken assets = jsonRoot["assets"];
|
||||
var fetched = JsonHelper.Deserialize(fetchedJson, SerializerContext.GithubReleasesJsonResponse);
|
||||
_buildVer = fetched.Name;
|
||||
|
||||
_buildVer = (string)jsonRoot["name"];
|
||||
|
||||
foreach (JToken asset in assets)
|
||||
foreach (var asset in fetched.Assets)
|
||||
{
|
||||
string assetName = (string)asset["name"];
|
||||
string assetState = (string)asset["state"];
|
||||
string downloadURL = (string)asset["browser_download_url"];
|
||||
|
||||
if (assetName.StartsWith("test-ava-ryujinx") && assetName.EndsWith(_platformExt))
|
||||
if (asset.Name.StartsWith("test-ava-ryujinx") && asset.Name.EndsWith(_platformExt))
|
||||
{
|
||||
_buildUrl = downloadURL;
|
||||
_buildUrl = asset.BrowserDownloadUrl;
|
||||
|
||||
if (assetState != "uploaded")
|
||||
if (asset.State != "uploaded")
|
||||
{
|
||||
if (showVersionUpToDate)
|
||||
{
|
||||
|
@@ -13,7 +13,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="$([MSBuild]::IsOSPlatform('OSX'))">
|
||||
<Exec Command="codesign --entitlements $(ProjectDir)..\distribution\macos\entitlements.xml -f --deep -s $(SigningCertificate) $(TargetDir)$(TargetName)" />
|
||||
<Exec Command="codesign --entitlements '$(ProjectDir)..\distribution\macos\entitlements.xml' -f --deep -s $(SigningCertificate) '$(TargetDir)$(TargetName)'" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' != ''">
|
||||
|
@@ -1,72 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
public class Amiibo
|
||||
{
|
||||
public struct AmiiboJson
|
||||
{
|
||||
[JsonPropertyName("amiibo")] public List<AmiiboApi> Amiibo { get; set; }
|
||||
[JsonPropertyName("lastUpdated")] public DateTime LastUpdated { get; set; }
|
||||
}
|
||||
|
||||
public struct AmiiboApi
|
||||
{
|
||||
[JsonPropertyName("name")] public string Name { get; set; }
|
||||
[JsonPropertyName("head")] public string Head { get; set; }
|
||||
[JsonPropertyName("tail")] public string Tail { get; set; }
|
||||
[JsonPropertyName("image")] public string Image { get; set; }
|
||||
[JsonPropertyName("amiiboSeries")] public string AmiiboSeries { get; set; }
|
||||
[JsonPropertyName("character")] public string Character { get; set; }
|
||||
[JsonPropertyName("gameSeries")] public string GameSeries { get; set; }
|
||||
[JsonPropertyName("type")] public string Type { get; set; }
|
||||
|
||||
[JsonPropertyName("release")] public Dictionary<string, string> Release { get; set; }
|
||||
|
||||
[JsonPropertyName("gamesSwitch")] public List<AmiiboApiGamesSwitch> GamesSwitch { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
public string GetId()
|
||||
{
|
||||
return Head + Tail;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is AmiiboApi amiibo)
|
||||
{
|
||||
return amiibo.Head + amiibo.Tail == Head + Tail;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return base.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
public class AmiiboApiGamesSwitch
|
||||
{
|
||||
[JsonPropertyName("amiiboUsage")] public List<AmiiboApiUsage> AmiiboUsage { get; set; }
|
||||
|
||||
[JsonPropertyName("gameID")] public List<string> GameId { get; set; }
|
||||
|
||||
[JsonPropertyName("gameName")] public string GameName { get; set; }
|
||||
}
|
||||
|
||||
public class AmiiboApiUsage
|
||||
{
|
||||
[JsonPropertyName("Usage")] public string Usage { get; set; }
|
||||
|
||||
[JsonPropertyName("write")] public bool Write { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using System.IO;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
@@ -21,6 +22,8 @@ namespace Ryujinx.Ava.UI.Models
|
||||
public string ContainerPath { get; }
|
||||
public string FullPath { get; }
|
||||
|
||||
public string FileName => Path.GetFileName(ContainerPath);
|
||||
|
||||
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
|
||||
{
|
||||
TitleId = titleId;
|
||||
|
@@ -122,7 +122,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/");
|
||||
|
||||
Supporters = string.Join(", ", JsonHelper.Deserialize<string[]>(patreonJsonString)) + "\n\n";
|
||||
Supporters = string.Join(", ", JsonHelper.Deserialize(patreonJsonString, CommonJsonContext.Default.StringArray) + "\n\n");
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@@ -4,11 +4,11 @@ using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Ui.Common.Models.Amiibo;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
@@ -17,6 +17,7 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AmiiboJsonSerializerContext = Ryujinx.Ui.Common.Models.Amiibo.AmiiboJsonSerializerContext;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
@@ -31,8 +32,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
private readonly StyleableWindow _owner;
|
||||
|
||||
private Bitmap _amiiboImage;
|
||||
private List<Amiibo.AmiiboApi> _amiiboList;
|
||||
private AvaloniaList<Amiibo.AmiiboApi> _amiibos;
|
||||
private List<AmiiboApi> _amiiboList;
|
||||
private AvaloniaList<AmiiboApi> _amiibos;
|
||||
private ObservableCollection<string> _amiiboSeries;
|
||||
|
||||
private int _amiiboSelectedIndex;
|
||||
@@ -42,6 +43,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
private bool _useRandomUuid;
|
||||
private string _usage;
|
||||
|
||||
private static readonly AmiiboJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
public AmiiboWindowViewModel(StyleableWindow owner, string lastScannedAmiiboId, string titleId)
|
||||
{
|
||||
_owner = owner;
|
||||
@@ -52,9 +55,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
Directory.CreateDirectory(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo"));
|
||||
|
||||
_amiiboJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", "Amiibo.json");
|
||||
_amiiboList = new List<Amiibo.AmiiboApi>();
|
||||
_amiiboList = new List<AmiiboApi>();
|
||||
_amiiboSeries = new ObservableCollection<string>();
|
||||
_amiibos = new AvaloniaList<Amiibo.AmiiboApi>();
|
||||
_amiibos = new AvaloniaList<AmiiboApi>();
|
||||
|
||||
_amiiboLogoBytes = EmbeddedResources.Read("Ryujinx.Ui.Common/Resources/Logo_Amiibo.png");
|
||||
|
||||
@@ -94,7 +97,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public AvaloniaList<Amiibo.AmiiboApi> AmiiboList
|
||||
public AvaloniaList<AmiiboApi> AmiiboList
|
||||
{
|
||||
get => _amiibos;
|
||||
set
|
||||
@@ -187,9 +190,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
if (File.Exists(_amiiboJsonPath))
|
||||
{
|
||||
amiiboJsonString = File.ReadAllText(_amiiboJsonPath);
|
||||
amiiboJsonString = await File.ReadAllTextAsync(_amiiboJsonPath);
|
||||
|
||||
if (await NeedsUpdate(JsonHelper.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).LastUpdated))
|
||||
if (await NeedsUpdate(JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).LastUpdated))
|
||||
{
|
||||
amiiboJsonString = await DownloadAmiiboJson();
|
||||
}
|
||||
@@ -206,7 +209,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
_amiiboList = JsonHelper.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).Amiibo;
|
||||
_amiiboList = JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).Amiibo;
|
||||
_amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
|
||||
|
||||
ParseAmiiboData();
|
||||
@@ -223,7 +226,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
if (!ShowAllAmiibo)
|
||||
{
|
||||
foreach (Amiibo.AmiiboApiGamesSwitch game in _amiiboList[i].GamesSwitch)
|
||||
foreach (AmiiboApiGamesSwitch game in _amiiboList[i].GamesSwitch)
|
||||
{
|
||||
if (game != null)
|
||||
{
|
||||
@@ -255,7 +258,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private void SelectLastScannedAmiibo()
|
||||
{
|
||||
Amiibo.AmiiboApi scanned = _amiiboList.FirstOrDefault(amiibo => amiibo.GetId() == LastScannedAmiiboId);
|
||||
AmiiboApi scanned = _amiiboList.FirstOrDefault(amiibo => amiibo.GetId() == LastScannedAmiiboId);
|
||||
|
||||
SeriesSelectedIndex = AmiiboSeries.IndexOf(scanned.AmiiboSeries);
|
||||
AmiiboSelectedIndex = AmiiboList.IndexOf(scanned);
|
||||
@@ -270,7 +273,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
List<Amiibo.AmiiboApi> amiiboSortedList = _amiiboList
|
||||
List<AmiiboApi> amiiboSortedList = _amiiboList
|
||||
.Where(amiibo => amiibo.AmiiboSeries == _amiiboSeries[SeriesSelectedIndex])
|
||||
.OrderBy(amiibo => amiibo.Name).ToList();
|
||||
|
||||
@@ -280,7 +283,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
if (!_showAllAmiibo)
|
||||
{
|
||||
foreach (Amiibo.AmiiboApiGamesSwitch game in amiiboSortedList[i].GamesSwitch)
|
||||
foreach (AmiiboApiGamesSwitch game in amiiboSortedList[i].GamesSwitch)
|
||||
{
|
||||
if (game != null)
|
||||
{
|
||||
@@ -314,7 +317,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
Amiibo.AmiiboApi selected = _amiibos[_amiiboSelectedIndex];
|
||||
AmiiboApi selected = _amiibos[_amiiboSelectedIndex];
|
||||
|
||||
string imageUrl = _amiiboList.FirstOrDefault(amiibo => amiibo.Equals(selected)).Image;
|
||||
|
||||
@@ -326,11 +329,11 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
bool writable = false;
|
||||
|
||||
foreach (Amiibo.AmiiboApiGamesSwitch item in _amiiboList[i].GamesSwitch)
|
||||
foreach (AmiiboApiGamesSwitch item in _amiiboList[i].GamesSwitch)
|
||||
{
|
||||
if (item.GameId.Contains(TitleId))
|
||||
{
|
||||
foreach (Amiibo.AmiiboApiUsage usageItem in item.AmiiboUsage)
|
||||
foreach (AmiiboApiUsage usageItem in item.AmiiboUsage)
|
||||
{
|
||||
usageString += Environment.NewLine +
|
||||
$"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}";
|
||||
|
@@ -51,6 +51,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
private bool _isLoaded;
|
||||
private readonly UserControl _owner;
|
||||
|
||||
private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
public IGamepadDriver AvaloniaKeyboardDriver { get; }
|
||||
public IGamepad SelectedGamepad { get; private set; }
|
||||
|
||||
@@ -706,10 +708,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
try
|
||||
{
|
||||
using (Stream stream = File.OpenRead(path))
|
||||
{
|
||||
config = JsonHelper.Deserialize<InputConfig>(stream);
|
||||
}
|
||||
config = JsonHelper.DeserializeFromFile(path, SerializerContext.InputConfig);
|
||||
}
|
||||
catch (JsonException) { }
|
||||
catch (InvalidOperationException)
|
||||
@@ -775,7 +774,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
config.ControllerType = Controllers[_controller].Type;
|
||||
|
||||
string jsonString = JsonHelper.Serialize(config, true);
|
||||
string jsonString = JsonHelper.Serialize(config, SerializerContext.InputConfig);
|
||||
|
||||
await File.WriteAllTextAsync(path, jsonString);
|
||||
|
||||
|
338
Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
Normal file
338
Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
Normal file
@@ -0,0 +1,338 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Threading;
|
||||
using DynamicData;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Tools.Fs;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public class DownloadableContentManagerViewModel : BaseModel
|
||||
{
|
||||
private readonly List<DownloadableContentContainer> _downloadableContentContainerList;
|
||||
private readonly string _downloadableContentJsonPath;
|
||||
|
||||
private VirtualFileSystem _virtualFileSystem;
|
||||
private AvaloniaList<DownloadableContentModel> _downloadableContents = new();
|
||||
private AvaloniaList<DownloadableContentModel> _views = new();
|
||||
private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
|
||||
|
||||
private string _search;
|
||||
private ulong _titleId;
|
||||
private string _titleName;
|
||||
|
||||
private static readonly DownloadableContentJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
public AvaloniaList<DownloadableContentModel> DownloadableContents
|
||||
{
|
||||
get => _downloadableContents;
|
||||
set
|
||||
{
|
||||
_downloadableContents = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(UpdateCount));
|
||||
Sort();
|
||||
}
|
||||
}
|
||||
|
||||
public AvaloniaList<DownloadableContentModel> Views
|
||||
{
|
||||
get => _views;
|
||||
set
|
||||
{
|
||||
_views = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public AvaloniaList<DownloadableContentModel> SelectedDownloadableContents
|
||||
{
|
||||
get => _selectedDownloadableContents;
|
||||
set
|
||||
{
|
||||
_selectedDownloadableContents = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string Search
|
||||
{
|
||||
get => _search;
|
||||
set
|
||||
{
|
||||
_search = value;
|
||||
OnPropertyChanged();
|
||||
Sort();
|
||||
}
|
||||
}
|
||||
|
||||
public string UpdateCount
|
||||
{
|
||||
get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count);
|
||||
}
|
||||
|
||||
public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||
{
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
|
||||
_titleId = titleId;
|
||||
_titleName = titleName;
|
||||
|
||||
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
|
||||
|
||||
try
|
||||
{
|
||||
_downloadableContentContainerList = JsonHelper.DeserializeFromFile(_downloadableContentJsonPath, SerializerContext.ListDownloadableContentContainer);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Configuration, "Downloadable Content JSON failed to deserialize.");
|
||||
_downloadableContentContainerList = new List<DownloadableContentContainer>();
|
||||
}
|
||||
|
||||
LoadDownloadableContents();
|
||||
}
|
||||
|
||||
private void LoadDownloadableContents()
|
||||
{
|
||||
foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
|
||||
{
|
||||
if (File.Exists(downloadableContentContainer.ContainerPath))
|
||||
{
|
||||
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
|
||||
|
||||
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
|
||||
|
||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||
|
||||
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
|
||||
{
|
||||
using UniqueRef<IFile> ncaFile = new();
|
||||
|
||||
partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath);
|
||||
if (nca != null)
|
||||
{
|
||||
var content = new DownloadableContentModel(nca.Header.TitleId.ToString("X16"),
|
||||
downloadableContentContainer.ContainerPath,
|
||||
downloadableContentNca.FullPath,
|
||||
downloadableContentNca.Enabled);
|
||||
|
||||
DownloadableContents.Add(content);
|
||||
|
||||
if (content.Enabled)
|
||||
{
|
||||
SelectedDownloadableContents.Add(content);
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(UpdateCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Save the list again to remove leftovers.
|
||||
Save();
|
||||
Sort();
|
||||
}
|
||||
|
||||
public void Sort()
|
||||
{
|
||||
DownloadableContents.AsObservableChangeSet()
|
||||
.Filter(Filter)
|
||||
.Bind(out var view).AsObservableList();
|
||||
|
||||
_views.Clear();
|
||||
_views.AddRange(view);
|
||||
OnPropertyChanged(nameof(Views));
|
||||
}
|
||||
|
||||
private bool Filter(object arg)
|
||||
{
|
||||
if (arg is DownloadableContentModel content)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(_search) || content.FileName.ToLower().Contains(_search.ToLower()) || content.TitleId.ToLower().Contains(_search.ToLower());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Nca TryOpenNca(IStorage ncaStorage, string containerPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new Nca(_virtualFileSystem.KeySet, ncaStorage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogLoadNcaErrorMessage], ex.Message, containerPath));
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async void Add()
|
||||
{
|
||||
OpenFileDialog dialog = new OpenFileDialog()
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.SelectDlcDialogTitle],
|
||||
AllowMultiple = true
|
||||
};
|
||||
|
||||
dialog.Filters.Add(new FileDialogFilter
|
||||
{
|
||||
Name = "NSP",
|
||||
Extensions = { "nsp" }
|
||||
});
|
||||
|
||||
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
string[] files = await dialog.ShowAsync(desktop.MainWindow);
|
||||
|
||||
if (files != null)
|
||||
{
|
||||
foreach (string file in files)
|
||||
{
|
||||
await AddDownloadableContent(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddDownloadableContent(string path)
|
||||
{
|
||||
if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using FileStream containerFile = File.OpenRead(path);
|
||||
|
||||
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
|
||||
bool containsDownloadableContent = false;
|
||||
|
||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||
{
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
|
||||
partitionFileSystem.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), path);
|
||||
if (nca == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||
{
|
||||
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var content = new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true);
|
||||
DownloadableContents.Add(content);
|
||||
SelectedDownloadableContents.Add(content);
|
||||
|
||||
OnPropertyChanged(nameof(UpdateCount));
|
||||
Sort();
|
||||
|
||||
containsDownloadableContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!containsDownloadableContent)
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(DownloadableContentModel model)
|
||||
{
|
||||
DownloadableContents.Remove(model);
|
||||
OnPropertyChanged(nameof(UpdateCount));
|
||||
Sort();
|
||||
}
|
||||
|
||||
public void RemoveAll()
|
||||
{
|
||||
DownloadableContents.Clear();
|
||||
OnPropertyChanged(nameof(UpdateCount));
|
||||
Sort();
|
||||
}
|
||||
|
||||
public void EnableAll()
|
||||
{
|
||||
SelectedDownloadableContents = new(DownloadableContents);
|
||||
}
|
||||
|
||||
public void DisableAll()
|
||||
{
|
||||
SelectedDownloadableContents.Clear();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
_downloadableContentContainerList.Clear();
|
||||
|
||||
DownloadableContentContainer container = default;
|
||||
|
||||
foreach (DownloadableContentModel downloadableContent in DownloadableContents)
|
||||
{
|
||||
if (container.ContainerPath != downloadableContent.ContainerPath)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
|
||||
{
|
||||
_downloadableContentContainerList.Add(container);
|
||||
}
|
||||
|
||||
container = new DownloadableContentContainer
|
||||
{
|
||||
ContainerPath = downloadableContent.ContainerPath,
|
||||
DownloadableContentNcaList = new List<DownloadableContentNca>()
|
||||
};
|
||||
}
|
||||
|
||||
container.DownloadableContentNcaList.Add(new DownloadableContentNca
|
||||
{
|
||||
Enabled = downloadableContent.Enabled,
|
||||
TitleId = Convert.ToUInt64(downloadableContent.TitleId, 16),
|
||||
FullPath = downloadableContent.FullPath
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
|
||||
{
|
||||
_downloadableContentContainerList.Add(container);
|
||||
}
|
||||
|
||||
JsonHelper.SerializeToFile(_downloadableContentJsonPath, _downloadableContentContainerList, SerializerContext.ListDownloadableContentContainer);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -1564,7 +1564,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
if (SelectedApplication != null)
|
||||
{
|
||||
await new DownloadableContentManagerWindow(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName).ShowDialog(TopLevel as Window);
|
||||
await DownloadableContentManagerWindow.Show(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -236,7 +236,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public DateTimeOffset DateOffset { get; set; }
|
||||
public TimeSpan TimeOffset { get; set; }
|
||||
private AvaloniaList<TimeZone> TimeZones { get; set; }
|
||||
internal AvaloniaList<TimeZone> TimeZones { get; set; }
|
||||
public AvaloniaList<string> GameDirectories { get; set; }
|
||||
public ObservableCollection<ComboBoxItem> AvailableGpus { get; set; }
|
||||
|
||||
|
@@ -25,10 +25,10 @@ using System.Text;
|
||||
using Path = System.IO.Path;
|
||||
using SpanHelpers = LibHac.Common.SpanHelpers;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels;
|
||||
|
||||
public class TitleUpdateViewModel : BaseModel
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public class TitleUpdateViewModel : BaseModel
|
||||
{
|
||||
public TitleUpdateMetadata _titleUpdateWindowData;
|
||||
public readonly string _titleUpdateJsonPath;
|
||||
private VirtualFileSystem _virtualFileSystem { get; }
|
||||
@@ -39,6 +39,8 @@ public class TitleUpdateViewModel : BaseModel
|
||||
private AvaloniaList<object> _views = new();
|
||||
private object _selectedUpdate;
|
||||
|
||||
private static readonly TitleUpdateMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
public AvaloniaList<TitleUpdateModel> TitleUpdates
|
||||
{
|
||||
get => _titleUpdates;
|
||||
@@ -80,7 +82,7 @@ public class TitleUpdateViewModel : BaseModel
|
||||
|
||||
try
|
||||
{
|
||||
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_titleUpdateJsonPath);
|
||||
_titleUpdateWindowData = JsonHelper.DeserializeFromFile(_titleUpdateJsonPath, SerializerContext.TitleUpdateMetadata);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -111,7 +113,6 @@ public class TitleUpdateViewModel : BaseModel
|
||||
|
||||
// NOTE: Save the list again to remove leftovers.
|
||||
Save();
|
||||
|
||||
SortUpdates();
|
||||
}
|
||||
|
||||
@@ -245,6 +246,7 @@ public class TitleUpdateViewModel : BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllBytes(_titleUpdateJsonPath, Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
|
||||
JsonHelper.SerializeToFile(_titleUpdateJsonPath, _titleUpdateWindowData, SerializerContext.TitleUpdateMetadata);
|
||||
}
|
||||
}
|
||||
}
|
@@ -42,7 +42,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
{
|
||||
string languageCode = Path.GetFileNameWithoutExtension(locale).Split('.').Last();
|
||||
string languageJson = EmbeddedResources.ReadAllText($"{localePath}/{languageCode}{localeExt}");
|
||||
var strings = JsonHelper.Deserialize<Dictionary<string, string>>(languageJson);
|
||||
var strings = JsonHelper.Deserialize(languageJson, CommonJsonContext.Default.StringDictionary);
|
||||
|
||||
if (!strings.TryGetValue("Language", out string languageName))
|
||||
{
|
||||
|
@@ -1,7 +1,7 @@
|
||||
using Avalonia.Interactivity;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ui.Common.Models.Amiibo;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
@@ -35,14 +35,14 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
}
|
||||
|
||||
public bool IsScanned { get; set; }
|
||||
public Amiibo.AmiiboApi ScannedAmiibo { get; set; }
|
||||
public AmiiboApi ScannedAmiibo { get; set; }
|
||||
public AmiiboWindowViewModel ViewModel { get; set; }
|
||||
|
||||
private void ScanButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ViewModel.AmiiboSelectedIndex > -1)
|
||||
{
|
||||
Amiibo.AmiiboApi amiibo = ViewModel.AmiiboList[ViewModel.AmiiboSelectedIndex];
|
||||
AmiiboApi amiibo = ViewModel.AmiiboList[ViewModel.AmiiboSelectedIndex];
|
||||
ScannedAmiibo = amiibo;
|
||||
IsScanned = true;
|
||||
Close();
|
||||
|
@@ -1,172 +1,194 @@
|
||||
<window:StyleableWindow
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Windows.DownloadableContentManagerWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||
Width="800"
|
||||
Height="500"
|
||||
MinWidth="800"
|
||||
MinHeight="500"
|
||||
MaxWidth="800"
|
||||
MaxHeight="500"
|
||||
SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
Width="500"
|
||||
Height="380"
|
||||
mc:Ignorable="d"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:DownloadableContentManagerViewModel"
|
||||
Focusable="True">
|
||||
<Grid Name="DownloadableContentGrid" Margin="15">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Panel
|
||||
Margin="0 0 0 10"
|
||||
Grid.Row="0">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Name="Heading"
|
||||
Grid.Row="1"
|
||||
MaxWidth="500"
|
||||
Margin="20,15,20,20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
LineHeight="18"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<DockPanel
|
||||
Grid.Row="2"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Left">
|
||||
Grid.Column="0"
|
||||
Text="{Binding UpdateCount}" />
|
||||
<StackPanel
|
||||
Margin="10 0"
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Name="EnableAllButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding EnableAll}">
|
||||
Command="{ReflectionBinding EnableAll}">
|
||||
<TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="DisableAllButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding DisableAll}">
|
||||
Command="{ReflectionBinding DisableAll}">
|
||||
<TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" />
|
||||
</Button>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
<TextBox
|
||||
Grid.Column="2"
|
||||
MinHeight="27"
|
||||
MaxHeight="27"
|
||||
HorizontalAlignment="Stretch"
|
||||
Watermark="{locale:Locale Search}"
|
||||
Text="{Binding Search}" />
|
||||
</Grid>
|
||||
</Panel>
|
||||
<Border
|
||||
Grid.Row="3"
|
||||
Margin="5"
|
||||
Grid.Row="1"
|
||||
Margin="0 0 0 24"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1">
|
||||
<ScrollViewer
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<DataGrid
|
||||
Name="DlcDataGrid"
|
||||
MinHeight="200"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
CanUserReorderColumns="False"
|
||||
CanUserResizeColumns="True"
|
||||
CanUserSortColumns="True"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
Items="{Binding _downloadableContents}"
|
||||
SelectionMode="Extended"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<DataGrid.Styles>
|
||||
<Styles>
|
||||
<Style Selector="DataGridCell:nth-child(3), DataGridCell:nth-child(4)">
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||
</Style>
|
||||
</Styles>
|
||||
<Styles>
|
||||
<Style Selector="DataGridCell:nth-child(1)">
|
||||
<Setter Property="HorizontalAlignment" Value="Right" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right" />
|
||||
</Style>
|
||||
</Styles>
|
||||
</DataGrid.Styles>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Width="90">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox
|
||||
Width="50"
|
||||
MinWidth="40"
|
||||
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Padding="2.5">
|
||||
<ListBox
|
||||
AutoScrollToSelectedItem="False"
|
||||
VirtualizationMode="None"
|
||||
SelectionMode="Multiple, Toggle"
|
||||
Background="Transparent"
|
||||
SelectionChanged="OnSelectionChanged"
|
||||
SelectedItems="{Binding SelectedDownloadableContents, Mode=TwoWay}"
|
||||
Items="{Binding Views}">
|
||||
<ListBox.DataTemplates>
|
||||
<DataTemplate
|
||||
DataType="models:DownloadableContentModel">
|
||||
<Panel Margin="10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid
|
||||
Grid.Column="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
MaxLines="2"
|
||||
TextWrapping="Wrap"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Text="{Binding FileName}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="10 0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding TitleId}" />
|
||||
</Grid>
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Spacing="10"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right">
|
||||
<Button
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
Padding="10"
|
||||
MinWidth="0"
|
||||
MinHeight="0"
|
||||
Click="OpenLocation">
|
||||
<ui:SymbolIcon
|
||||
Symbol="OpenFolder"
|
||||
HorizontalAlignment="Center"
|
||||
IsChecked="{Binding Enabled}" />
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
<Button
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
Padding="10"
|
||||
MinWidth="0"
|
||||
MinHeight="0"
|
||||
Click="RemoveDLC">
|
||||
<ui:SymbolIcon
|
||||
Symbol="Cancel"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
<DataGridTemplateColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingEnabledLabel}" />
|
||||
</DataGridTemplateColumn.Header>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn Width="140" Binding="{Binding TitleId}">
|
||||
<DataGridTextColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingTitleIdLabel}" />
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn Width="280" Binding="{Binding FullPath}">
|
||||
<DataGridTextColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingFullPathLabel}" />
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn Binding="{Binding ContainerPath}">
|
||||
<DataGridTextColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingContainerPathLabel}" />
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</ScrollViewer>
|
||||
</ListBox.DataTemplates>
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
</ListBox>
|
||||
</Border>
|
||||
<DockPanel
|
||||
Grid.Row="4"
|
||||
Margin="0"
|
||||
<Panel
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Stretch">
|
||||
<DockPanel Margin="0" HorizontalAlignment="Left">
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Left">
|
||||
<Button
|
||||
Name="AddButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding Add}">
|
||||
Command="{ReflectionBinding Add}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="RemoveButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding RemoveSelected}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="RemoveAllButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding RemoveAll}">
|
||||
Command="{ReflectionBinding RemoveAll}">
|
||||
<TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
|
||||
</Button>
|
||||
</DockPanel>
|
||||
<DockPanel Margin="0" HorizontalAlignment="Right">
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Right">
|
||||
<Button
|
||||
Name="SaveButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding SaveAndClose}">
|
||||
Click="SaveAndClose">
|
||||
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="CancelButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding Close}">
|
||||
Click="Close">
|
||||
<TextBlock Text="{locale:Locale InputDialogCancel}" />
|
||||
</Button>
|
||||
</DockPanel>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Grid>
|
||||
</window:StyleableWindow>
|
||||
</UserControl>
|
@@ -1,314 +1,115 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Tools.Fs;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Styling;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System.Threading.Tasks;
|
||||
using Path = System.IO.Path;
|
||||
using Button = Avalonia.Controls.Button;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class DownloadableContentManagerWindow : StyleableWindow
|
||||
public partial class DownloadableContentManagerWindow : UserControl
|
||||
{
|
||||
private readonly List<DownloadableContentContainer> _downloadableContentContainerList;
|
||||
private readonly string _downloadableContentJsonPath;
|
||||
|
||||
private VirtualFileSystem _virtualFileSystem { get; }
|
||||
private AvaloniaList<DownloadableContentModel> _downloadableContents { get; set; }
|
||||
|
||||
private ulong _titleId { get; }
|
||||
private string _titleName { get; }
|
||||
public DownloadableContentManagerViewModel ViewModel;
|
||||
|
||||
public DownloadableContentManagerWindow()
|
||||
{
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.DlcWindowTitle]} - {_titleName} ({_titleId:X16})";
|
||||
}
|
||||
|
||||
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||
{
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_downloadableContents = new AvaloniaList<DownloadableContentModel>();
|
||||
|
||||
_titleId = titleId;
|
||||
_titleName = titleName;
|
||||
|
||||
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
|
||||
|
||||
try
|
||||
{
|
||||
_downloadableContentContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(_downloadableContentJsonPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_downloadableContentContainerList = new List<DownloadableContentContainer>();
|
||||
}
|
||||
|
||||
DataContext = this;
|
||||
DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, titleId, titleName);
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
RemoveButton.IsEnabled = false;
|
||||
|
||||
DlcDataGrid.SelectionChanged += DlcDataGrid_SelectionChanged;
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.DlcWindowTitle]} - {_titleName} ({_titleId:X16})";
|
||||
|
||||
LoadDownloadableContents();
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
private void DlcDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||
{
|
||||
RemoveButton.IsEnabled = (DlcDataGrid.SelectedItems.Count > 0);
|
||||
}
|
||||
|
||||
private void PrintHeading()
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
Heading.Text = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DlcWindowHeading, _downloadableContents.Count, _titleName, _titleId.ToString("X16"));
|
||||
}
|
||||
|
||||
private void LoadDownloadableContents()
|
||||
{
|
||||
foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
|
||||
{
|
||||
if (File.Exists(downloadableContentContainer.ContainerPath))
|
||||
{
|
||||
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
|
||||
|
||||
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
|
||||
|
||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||
|
||||
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
|
||||
{
|
||||
using UniqueRef<IFile> ncaFile = new();
|
||||
|
||||
partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath);
|
||||
if (nca != null)
|
||||
{
|
||||
_downloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"),
|
||||
downloadableContentContainer.ContainerPath,
|
||||
downloadableContentNca.FullPath,
|
||||
downloadableContentNca.Enabled));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Save the list again to remove leftovers.
|
||||
Save();
|
||||
}
|
||||
|
||||
private Nca TryOpenNca(IStorage ncaStorage, string containerPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new Nca(_virtualFileSystem.KeySet, ncaStorage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, containerPath));
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task AddDownloadableContent(string path)
|
||||
{
|
||||
if (!File.Exists(path) || _downloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using FileStream containerFile = File.OpenRead(path);
|
||||
|
||||
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
|
||||
bool containsDownloadableContent = false;
|
||||
|
||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||
{
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
|
||||
partitionFileSystem.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), path);
|
||||
if (nca == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||
{
|
||||
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
_downloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true));
|
||||
|
||||
containsDownloadableContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!containsDownloadableContent)
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveDownloadableContents(bool removeSelectedOnly = false)
|
||||
{
|
||||
if (removeSelectedOnly)
|
||||
{
|
||||
AvaloniaList<DownloadableContentModel> removedItems = new();
|
||||
|
||||
foreach (var item in DlcDataGrid.SelectedItems)
|
||||
{
|
||||
removedItems.Add(item as DownloadableContentModel);
|
||||
}
|
||||
|
||||
DlcDataGrid.SelectedItems.Clear();
|
||||
|
||||
foreach (var item in removedItems)
|
||||
{
|
||||
_downloadableContents.RemoveAll(_downloadableContents.Where(x => x.TitleId == item.TitleId).ToList());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_downloadableContents.Clear();
|
||||
}
|
||||
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
public void RemoveSelected()
|
||||
{
|
||||
RemoveDownloadableContents(true);
|
||||
}
|
||||
|
||||
public void RemoveAll()
|
||||
{
|
||||
RemoveDownloadableContents();
|
||||
}
|
||||
|
||||
public void EnableAll()
|
||||
{
|
||||
foreach(var item in _downloadableContents)
|
||||
{
|
||||
item.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void DisableAll()
|
||||
{
|
||||
foreach (var item in _downloadableContents)
|
||||
{
|
||||
item.Enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async void Add()
|
||||
{
|
||||
OpenFileDialog dialog = new OpenFileDialog()
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.SelectDlcDialogTitle],
|
||||
AllowMultiple = true
|
||||
PrimaryButtonText = "",
|
||||
SecondaryButtonText = "",
|
||||
CloseButtonText = "",
|
||||
Content = new DownloadableContentManagerWindow(virtualFileSystem, titleId, titleName),
|
||||
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], titleName, titleId.ToString("X16"))
|
||||
};
|
||||
|
||||
dialog.Filters.Add(new FileDialogFilter
|
||||
{
|
||||
Name = "NSP",
|
||||
Extensions = { "nsp" }
|
||||
});
|
||||
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
||||
bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false));
|
||||
|
||||
string[] files = await dialog.ShowAsync(this);
|
||||
contentDialog.Styles.Add(bottomBorder);
|
||||
|
||||
if (files != null)
|
||||
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||
}
|
||||
|
||||
private void SaveAndClose(object sender, RoutedEventArgs routedEventArgs)
|
||||
{
|
||||
foreach (string file in files)
|
||||
ViewModel.Save();
|
||||
((ContentDialog)Parent).Hide();
|
||||
}
|
||||
|
||||
private void Close(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await AddDownloadableContent(file);
|
||||
((ContentDialog)Parent).Hide();
|
||||
}
|
||||
|
||||
private void RemoveDLC(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
if (button.DataContext is DownloadableContentModel model)
|
||||
{
|
||||
ViewModel.Remove(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PrintHeading();
|
||||
private void OpenLocation(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
if (button.DataContext is DownloadableContentModel model)
|
||||
{
|
||||
OpenHelper.LocateFile(model.ContainerPath);
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
_downloadableContentContainerList.Clear();
|
||||
|
||||
DownloadableContentContainer container = default;
|
||||
|
||||
foreach (DownloadableContentModel downloadableContent in _downloadableContents)
|
||||
{
|
||||
if (container.ContainerPath != downloadableContent.ContainerPath)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
|
||||
{
|
||||
_downloadableContentContainerList.Add(container);
|
||||
}
|
||||
|
||||
container = new DownloadableContentContainer
|
||||
{
|
||||
ContainerPath = downloadableContent.ContainerPath,
|
||||
DownloadableContentNcaList = new List<DownloadableContentNca>()
|
||||
};
|
||||
}
|
||||
|
||||
container.DownloadableContentNcaList.Add(new DownloadableContentNca
|
||||
{
|
||||
Enabled = downloadableContent.Enabled,
|
||||
TitleId = Convert.ToUInt64(downloadableContent.TitleId, 16),
|
||||
FullPath = downloadableContent.FullPath
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
|
||||
{
|
||||
_downloadableContentContainerList.Add(container);
|
||||
}
|
||||
|
||||
using (FileStream downloadableContentJsonStream = File.Create(_downloadableContentJsonPath, 4096, FileOptions.WriteThrough))
|
||||
{
|
||||
downloadableContentJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_downloadableContentContainerList, true)));
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveAndClose()
|
||||
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
Save();
|
||||
Close();
|
||||
foreach (var content in e.AddedItems)
|
||||
{
|
||||
if (content is DownloadableContentModel model)
|
||||
{
|
||||
var index = ViewModel.DownloadableContents.IndexOf(model);
|
||||
|
||||
if (index != -1)
|
||||
{
|
||||
ViewModel.DownloadableContents[index].Enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var content in e.RemovedItems)
|
||||
{
|
||||
if (content is DownloadableContentModel model)
|
||||
{
|
||||
var index = ViewModel.DownloadableContents.IndexOf(model);
|
||||
|
||||
if (index != -1)
|
||||
{
|
||||
ViewModel.DownloadableContents[index].Enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -125,7 +125,7 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
|
||||
public static Bgra32[] GetBuffer(Image<Bgra32> image)
|
||||
{
|
||||
return image.TryGetSinglePixelSpan(out var data) ? data.ToArray() : new Bgra32[0];
|
||||
return image.TryGetSinglePixelSpan(out var data) ? data.ToArray() : Array.Empty<Bgra32>();
|
||||
}
|
||||
|
||||
private static int GetColorScore(Dictionary<int, int> dominantColorBin, int maxHitCount, PaletteColor color)
|
||||
|
@@ -6,11 +6,8 @@ using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Button = Avalonia.Controls.Button;
|
||||
|
||||
|
@@ -1,5 +1,9 @@
|
||||
namespace Ryujinx.Common.Configuration
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<AspectRatio>))]
|
||||
public enum AspectRatio
|
||||
{
|
||||
Fixed4x3,
|
||||
|
@@ -1,5 +1,9 @@
|
||||
namespace Ryujinx.Common.Configuration
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<BackendThreading>))]
|
||||
public enum BackendThreading
|
||||
{
|
||||
Auto,
|
||||
|
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration
|
||||
{
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
[JsonSerializable(typeof(List<DownloadableContentContainer>))]
|
||||
public partial class DownloadableContentJsonSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
@@ -1,5 +1,9 @@
|
||||
namespace Ryujinx.Common.Configuration
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<GraphicsBackend>))]
|
||||
public enum GraphicsBackend
|
||||
{
|
||||
Vulkan,
|
||||
|
@@ -1,5 +1,9 @@
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<GraphicsDebugLevel>))]
|
||||
public enum GraphicsDebugLevel
|
||||
{
|
||||
None,
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -6,6 +7,8 @@ namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
|
||||
{
|
||||
class JsonMotionConfigControllerConverter : JsonConverter<MotionConfigController>
|
||||
{
|
||||
private static readonly MotionConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
private static MotionInputBackendType GetMotionInputBackendType(ref Utf8JsonReader reader)
|
||||
{
|
||||
// Temporary reader to get the backend type
|
||||
@@ -52,8 +55,8 @@ namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
|
||||
|
||||
return motionBackendType switch
|
||||
{
|
||||
MotionInputBackendType.GamepadDriver => (MotionConfigController)JsonSerializer.Deserialize(ref reader, typeof(StandardMotionConfigController), options),
|
||||
MotionInputBackendType.CemuHook => (MotionConfigController)JsonSerializer.Deserialize(ref reader, typeof(CemuHookMotionConfigController), options),
|
||||
MotionInputBackendType.GamepadDriver => JsonSerializer.Deserialize(ref reader, SerializerContext.StandardMotionConfigController),
|
||||
MotionInputBackendType.CemuHook => JsonSerializer.Deserialize(ref reader, SerializerContext.CemuHookMotionConfigController),
|
||||
_ => throw new InvalidOperationException($"Unknown backend type {motionBackendType}"),
|
||||
};
|
||||
}
|
||||
@@ -63,10 +66,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
|
||||
switch (value.MotionBackend)
|
||||
{
|
||||
case MotionInputBackendType.GamepadDriver:
|
||||
JsonSerializer.Serialize(writer, value as StandardMotionConfigController, options);
|
||||
JsonSerializer.Serialize(writer, value as StandardMotionConfigController, SerializerContext.StandardMotionConfigController);
|
||||
break;
|
||||
case MotionInputBackendType.CemuHook:
|
||||
JsonSerializer.Serialize(writer, value as CemuHookMotionConfigController, options);
|
||||
JsonSerializer.Serialize(writer, value as CemuHookMotionConfigController, SerializerContext.CemuHookMotionConfigController);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Unknown motion backend type {value.MotionBackend}");
|
||||
|
@@ -1,5 +1,8 @@
|
||||
namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
|
||||
{
|
||||
[JsonConverter(typeof(JsonMotionConfigControllerConverter))]
|
||||
public class MotionConfigController
|
||||
{
|
||||
public MotionInputBackendType MotionBackend { get; set; }
|
||||
|
@@ -0,0 +1,12 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
|
||||
{
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
[JsonSerializable(typeof(MotionConfigController))]
|
||||
[JsonSerializable(typeof(CemuHookMotionConfigController))]
|
||||
[JsonSerializable(typeof(StandardMotionConfigController))]
|
||||
public partial class MotionConfigJsonSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
@@ -1,5 +1,9 @@
|
||||
namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<MotionInputBackendType>))]
|
||||
public enum MotionInputBackendType : byte
|
||||
{
|
||||
Invalid,
|
||||
|
@@ -1,9 +1,12 @@
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration.Hid
|
||||
{
|
||||
[Flags]
|
||||
// This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical
|
||||
[Flags]
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<ControllerType>))]
|
||||
public enum ControllerType : int
|
||||
{
|
||||
None,
|
||||
|
@@ -1,5 +1,9 @@
|
||||
namespace Ryujinx.Common.Configuration.Hid
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration.Hid
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<InputBackendType>))]
|
||||
public enum InputBackendType
|
||||
{
|
||||
Invalid,
|
||||
|
@@ -1,8 +1,10 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration.Hid
|
||||
{
|
||||
[JsonConverter(typeof(JsonInputConfigConverter))]
|
||||
public class InputConfig : INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>
|
||||
|
@@ -0,0 +1,14 @@
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration.Hid
|
||||
{
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
[JsonSerializable(typeof(InputConfig))]
|
||||
[JsonSerializable(typeof(StandardKeyboardInputConfig))]
|
||||
[JsonSerializable(typeof(StandardControllerInputConfig))]
|
||||
public partial class InputConfigJsonSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
@@ -1,13 +1,16 @@
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration.Hid
|
||||
{
|
||||
class JsonInputConfigConverter : JsonConverter<InputConfig>
|
||||
public class JsonInputConfigConverter : JsonConverter<InputConfig>
|
||||
{
|
||||
private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
private static InputBackendType GetInputBackendType(ref Utf8JsonReader reader)
|
||||
{
|
||||
// Temporary reader to get the backend type
|
||||
@@ -54,8 +57,8 @@ namespace Ryujinx.Common.Configuration.Hid
|
||||
|
||||
return backendType switch
|
||||
{
|
||||
InputBackendType.WindowKeyboard => (InputConfig)JsonSerializer.Deserialize(ref reader, typeof(StandardKeyboardInputConfig), options),
|
||||
InputBackendType.GamepadSDL2 => (InputConfig)JsonSerializer.Deserialize(ref reader, typeof(StandardControllerInputConfig), options),
|
||||
InputBackendType.WindowKeyboard => JsonSerializer.Deserialize(ref reader, SerializerContext.StandardKeyboardInputConfig),
|
||||
InputBackendType.GamepadSDL2 => JsonSerializer.Deserialize(ref reader, SerializerContext.StandardControllerInputConfig),
|
||||
_ => throw new InvalidOperationException($"Unknown backend type {backendType}"),
|
||||
};
|
||||
}
|
||||
@@ -65,10 +68,10 @@ namespace Ryujinx.Common.Configuration.Hid
|
||||
switch (value.Backend)
|
||||
{
|
||||
case InputBackendType.WindowKeyboard:
|
||||
JsonSerializer.Serialize(writer, value as StandardKeyboardInputConfig, options);
|
||||
JsonSerializer.Serialize(writer, value as StandardKeyboardInputConfig, SerializerContext.StandardKeyboardInputConfig);
|
||||
break;
|
||||
case InputBackendType.GamepadSDL2:
|
||||
JsonSerializer.Serialize(writer, value as StandardControllerInputConfig, options);
|
||||
JsonSerializer.Serialize(writer, value as StandardControllerInputConfig, SerializerContext.StandardControllerInputConfig);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Unknown backend type {value.Backend}");
|
||||
|
@@ -1,6 +1,10 @@
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration.Hid
|
||||
{
|
||||
// This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<PlayerIndex>))]
|
||||
public enum PlayerIndex : int
|
||||
{
|
||||
Player1 = 0,
|
||||
|
@@ -1,5 +1,9 @@
|
||||
namespace Ryujinx.Common.Configuration
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<MemoryManagerMode>))]
|
||||
public enum MemoryManagerMode : byte
|
||||
{
|
||||
SoftwarePageTable,
|
||||
|
@@ -0,0 +1,10 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration
|
||||
{
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
[JsonSerializable(typeof(TitleUpdateMetadata))]
|
||||
public partial class TitleUpdateMetadataJsonSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
@@ -12,19 +12,5 @@ namespace Ryujinx.Common
|
||||
{
|
||||
return MemoryMarshal.Cast<byte, T>(reader.ReadBytes(Unsafe.SizeOf<T>()))[0];
|
||||
}
|
||||
|
||||
public unsafe static void WriteStruct<T>(this BinaryWriter writer, T value)
|
||||
where T : unmanaged
|
||||
{
|
||||
ReadOnlySpan<byte> data = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateReadOnlySpan(ref value, 1));
|
||||
|
||||
writer.Write(data);
|
||||
}
|
||||
|
||||
public static void Write(this BinaryWriter writer, UInt128 value)
|
||||
{
|
||||
writer.Write((ulong)value);
|
||||
writer.Write((ulong)(value >> 64));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
28
Ryujinx.Common/Extensions/BinaryWriterExtensions.cs
Normal file
28
Ryujinx.Common/Extensions/BinaryWriterExtensions.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Common
|
||||
{
|
||||
public static class BinaryWriterExtensions
|
||||
{
|
||||
public unsafe static void WriteStruct<T>(this BinaryWriter writer, T value)
|
||||
where T : unmanaged
|
||||
{
|
||||
ReadOnlySpan<byte> data = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateReadOnlySpan(ref value, 1));
|
||||
|
||||
writer.Write(data);
|
||||
}
|
||||
|
||||
public static void Write(this BinaryWriter writer, UInt128 value)
|
||||
{
|
||||
writer.Write((ulong)value);
|
||||
writer.Write((ulong)(value >> 64));
|
||||
}
|
||||
|
||||
public static void Write(this BinaryWriter writer, MemoryStream stream)
|
||||
{
|
||||
stream.CopyTo(writer.BaseStream);
|
||||
}
|
||||
}
|
||||
}
|
138
Ryujinx.Common/Extensions/StreamExtensions.cs
Normal file
138
Ryujinx.Common/Extensions/StreamExtensions.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Common
|
||||
{
|
||||
public static class StreamExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes a <cref="ReadOnlySpan<int>" /> to this stream.
|
||||
///
|
||||
/// This default implementation converts each buffer value to a stack-allocated
|
||||
/// byte array, then writes it to the Stream using <cref="System.Stream.Write(byte[])" />.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to be written to</param>
|
||||
/// <param name="buffer">The buffer of values to be written</param>
|
||||
public static void Write(this Stream stream, ReadOnlySpan<int> buffer)
|
||||
{
|
||||
if (buffer.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
ReadOnlySpan<byte> byteBuffer = MemoryMarshal.Cast<int, byte>(buffer);
|
||||
stream.Write(byteBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
Span<byte> byteBuffer = stackalloc byte[sizeof(int)];
|
||||
|
||||
foreach (int value in buffer)
|
||||
{
|
||||
BinaryPrimitives.WriteInt32LittleEndian(byteBuffer, value);
|
||||
stream.Write(byteBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a four-byte signed integer to this stream. The current position
|
||||
/// of the stream is advanced by four.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to be written to</param>
|
||||
/// <param name="value">The value to be written</param>
|
||||
public static void Write(this Stream stream, int value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(buffer, value);
|
||||
stream.Write(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an eight-byte signed integer to this stream. The current position
|
||||
/// of the stream is advanced by eight.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to be written to</param>
|
||||
/// <param name="value">The value to be written</param>
|
||||
public static void Write(this Stream stream, long value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(long)];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
|
||||
stream.Write(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
// Writes a four-byte unsigned integer to this stream. The current position
|
||||
// of the stream is advanced by four.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to be written to</param>
|
||||
/// <param name="value">The value to be written</param>
|
||||
public static void Write(this Stream stream, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
stream.Write(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an eight-byte unsigned integer to this stream. The current
|
||||
/// position of the stream is advanced by eight.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to be written to</param>
|
||||
/// <param name="value">The value to be written</param>
|
||||
public static void Write(this Stream stream, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
stream.Write(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the contents of source to stream by calling source.CopyTo(stream).
|
||||
/// Provides consistency with other Stream.Write methods.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to be written to</param>
|
||||
/// <param name="source">The stream to be read from</param>
|
||||
public static void Write(this Stream stream, Stream source)
|
||||
{
|
||||
source.CopyTo(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a sequence of bytes to the Stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to be written to.</param>
|
||||
/// <param name="value">The byte to be written</param>
|
||||
/// <param name="count">The number of times the value should be written</param>
|
||||
public static void WriteByte(this Stream stream, byte value, int count)
|
||||
{
|
||||
if (count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int BlockSize = 16;
|
||||
|
||||
int blockCount = count / BlockSize;
|
||||
if (blockCount > 0)
|
||||
{
|
||||
Span<byte> span = stackalloc byte[BlockSize];
|
||||
span.Fill(value);
|
||||
for (int x = 0; x < blockCount; x++)
|
||||
{
|
||||
stream.Write(span);
|
||||
}
|
||||
}
|
||||
|
||||
int nonBlockBytes = count % BlockSize;
|
||||
for (int x = 0; x < nonBlockBytes; x++)
|
||||
{
|
||||
stream.WriteByte(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,22 +1,20 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Common.Logging
|
||||
{
|
||||
internal class DefaultLogFormatter : ILogFormatter
|
||||
{
|
||||
private static readonly ObjectPool<StringBuilder> _stringBuilderPool = SharedPools.Default<StringBuilder>();
|
||||
private static readonly ObjectPool<StringBuilder> StringBuilderPool = SharedPools.Default<StringBuilder>();
|
||||
|
||||
public string Format(LogEventArgs args)
|
||||
{
|
||||
StringBuilder sb = _stringBuilderPool.Allocate();
|
||||
StringBuilder sb = StringBuilderPool.Allocate();
|
||||
|
||||
try
|
||||
{
|
||||
sb.Clear();
|
||||
|
||||
sb.AppendFormat(@"{0:hh\:mm\:ss\.fff}", args.Time);
|
||||
sb.Append($@"{args.Time:hh\:mm\:ss\.fff}");
|
||||
sb.Append($" |{args.Level.ToString()[0]}| ");
|
||||
|
||||
if (args.ThreadName != null)
|
||||
@@ -27,53 +25,17 @@ namespace Ryujinx.Common.Logging
|
||||
|
||||
sb.Append(args.Message);
|
||||
|
||||
if (args.Data != null)
|
||||
if (args.Data is not null)
|
||||
{
|
||||
PropertyInfo[] props = args.Data.GetType().GetProperties();
|
||||
|
||||
sb.Append(" {");
|
||||
|
||||
foreach (var prop in props)
|
||||
{
|
||||
sb.Append(prop.Name);
|
||||
sb.Append(": ");
|
||||
|
||||
if (typeof(Array).IsAssignableFrom(prop.PropertyType))
|
||||
{
|
||||
Array array = (Array)prop.GetValue(args.Data);
|
||||
foreach (var item in array)
|
||||
{
|
||||
sb.Append(item.ToString());
|
||||
sb.Append(", ");
|
||||
}
|
||||
|
||||
if (array.Length > 0)
|
||||
{
|
||||
sb.Remove(sb.Length - 2, 2);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(prop.GetValue(args.Data));
|
||||
}
|
||||
|
||||
sb.Append(" ; ");
|
||||
}
|
||||
|
||||
// We remove the final ';' from the string
|
||||
if (props.Length > 0)
|
||||
{
|
||||
sb.Remove(sb.Length - 3, 3);
|
||||
}
|
||||
|
||||
sb.Append('}');
|
||||
sb.Append(' ');
|
||||
DynamicObjectFormatter.Format(sb, args.Data);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stringBuilderPool.Release(sb);
|
||||
StringBuilderPool.Release(sb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
84
Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs
Normal file
84
Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Common.Logging
|
||||
{
|
||||
internal class DynamicObjectFormatter
|
||||
{
|
||||
private static readonly ObjectPool<StringBuilder> StringBuilderPool = SharedPools.Default<StringBuilder>();
|
||||
|
||||
public static string? Format(object? dynamicObject)
|
||||
{
|
||||
if (dynamicObject is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder sb = StringBuilderPool.Allocate();
|
||||
|
||||
try
|
||||
{
|
||||
Format(sb, dynamicObject);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
finally
|
||||
{
|
||||
StringBuilderPool.Release(sb);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Format(StringBuilder sb, object? dynamicObject)
|
||||
{
|
||||
if (dynamicObject is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PropertyInfo[] props = dynamicObject.GetType().GetProperties();
|
||||
|
||||
sb.Append('{');
|
||||
|
||||
foreach (var prop in props)
|
||||
{
|
||||
sb.Append(prop.Name);
|
||||
sb.Append(": ");
|
||||
|
||||
if (typeof(Array).IsAssignableFrom(prop.PropertyType))
|
||||
{
|
||||
Array? array = (Array?) prop.GetValue(dynamicObject);
|
||||
|
||||
if (array is not null)
|
||||
{
|
||||
foreach (var item in array)
|
||||
{
|
||||
sb.Append(item);
|
||||
sb.Append(", ");
|
||||
}
|
||||
|
||||
if (array.Length > 0)
|
||||
{
|
||||
sb.Remove(sb.Length - 2, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(prop.GetValue(dynamicObject));
|
||||
}
|
||||
|
||||
sb.Append(" ; ");
|
||||
}
|
||||
|
||||
// We remove the final ';' from the string
|
||||
if (props.Length > 0)
|
||||
{
|
||||
sb.Remove(sb.Length - 3, 3);
|
||||
}
|
||||
|
||||
sb.Append('}');
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,9 @@
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Logging
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<LogClass>))]
|
||||
public enum LogClass
|
||||
{
|
||||
Application,
|
||||
|
@@ -11,15 +11,7 @@ namespace Ryujinx.Common.Logging
|
||||
public readonly string Message;
|
||||
public readonly object Data;
|
||||
|
||||
public LogEventArgs(LogLevel level, TimeSpan time, string threadName, string message)
|
||||
{
|
||||
Level = level;
|
||||
Time = time;
|
||||
ThreadName = threadName;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public LogEventArgs(LogLevel level, TimeSpan time, string threadName, string message, object data)
|
||||
public LogEventArgs(LogLevel level, TimeSpan time, string threadName, string message, object data = null)
|
||||
{
|
||||
Level = level;
|
||||
Time = time;
|
||||
|
30
Ryujinx.Common/Logging/LogEventArgsJson.cs
Normal file
30
Ryujinx.Common/Logging/LogEventArgsJson.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Logging
|
||||
{
|
||||
internal class LogEventArgsJson
|
||||
{
|
||||
public LogLevel Level { get; }
|
||||
public TimeSpan Time { get; }
|
||||
public string ThreadName { get; }
|
||||
|
||||
public string Message { get; }
|
||||
public string Data { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
public LogEventArgsJson(LogLevel level, TimeSpan time, string threadName, string message, string data = null)
|
||||
{
|
||||
Level = level;
|
||||
Time = time;
|
||||
ThreadName = threadName;
|
||||
Message = message;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public static LogEventArgsJson FromLogEventArgs(LogEventArgs args)
|
||||
{
|
||||
return new LogEventArgsJson(args.Level, args.Time, args.ThreadName, args.Message, DynamicObjectFormatter.Format(args.Data));
|
||||
}
|
||||
}
|
||||
}
|
9
Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs
Normal file
9
Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Logging
|
||||
{
|
||||
[JsonSerializable(typeof(LogEventArgsJson))]
|
||||
internal partial class LogEventJsonSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
@@ -1,5 +1,9 @@
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Logging
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<LogLevel>))]
|
||||
public enum LogLevel
|
||||
{
|
||||
Debug,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System.IO;
|
||||
|
||||
namespace Ryujinx.Common.Logging
|
||||
{
|
||||
@@ -25,12 +25,8 @@ namespace Ryujinx.Common.Logging
|
||||
|
||||
public void Log(object sender, LogEventArgs e)
|
||||
{
|
||||
string text = JsonSerializer.Serialize(e);
|
||||
|
||||
using (BinaryWriter writer = new BinaryWriter(_stream))
|
||||
{
|
||||
writer.Write(text);
|
||||
}
|
||||
var logEventArgsJson = LogEventArgsJson.FromLogEventArgs(e);
|
||||
JsonHelper.SerializeToStream(_stream, logEventArgsJson, LogEventJsonSerializerContext.Default.LogEventArgsJson);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
99
Ryujinx.Common/Memory/MemoryStreamManager.cs
Normal file
99
Ryujinx.Common/Memory/MemoryStreamManager.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using Microsoft.IO;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Common.Memory
|
||||
{
|
||||
public static class MemoryStreamManager
|
||||
{
|
||||
private static readonly RecyclableMemoryStreamManager _shared = new RecyclableMemoryStreamManager();
|
||||
|
||||
/// <summary>
|
||||
/// We don't expose the <c>RecyclableMemoryStreamManager</c> directly because version 2.x
|
||||
/// returns them as <c>MemoryStream</c>. This Shared class is here to a) offer only the GetStream() versions we use
|
||||
/// and b) return them as <c>RecyclableMemoryStream</c> so we don't have to cast.
|
||||
/// </summary>
|
||||
public static class Shared
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve a new <c>MemoryStream</c> object with no tag and a default initial capacity.
|
||||
/// </summary>
|
||||
/// <returns>A <c>RecyclableMemoryStream</c></returns>
|
||||
public static RecyclableMemoryStream GetStream()
|
||||
=> new RecyclableMemoryStream(_shared);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a new <c>MemoryStream</c> object with the contents copied from the provided
|
||||
/// buffer. The provided buffer is not wrapped or used after construction.
|
||||
/// </summary>
|
||||
/// <remarks>The new stream's position is set to the beginning of the stream when returned.</remarks>
|
||||
/// <param name="buffer">The byte buffer to copy data from</param>
|
||||
/// <returns>A <c>RecyclableMemoryStream</c></returns>
|
||||
public static RecyclableMemoryStream GetStream(byte[] buffer)
|
||||
=> GetStream(Guid.NewGuid(), null, buffer, 0, buffer.Length);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a new <c>MemoryStream</c> object with the given tag and with contents copied from the provided
|
||||
/// buffer. The provided buffer is not wrapped or used after construction.
|
||||
/// </summary>
|
||||
/// <remarks>The new stream's position is set to the beginning of the stream when returned.</remarks>
|
||||
/// <param name="buffer">The byte buffer to copy data from</param>
|
||||
/// <returns>A <c>RecyclableMemoryStream</c></returns>
|
||||
public static RecyclableMemoryStream GetStream(ReadOnlySpan<byte> buffer)
|
||||
=> GetStream(Guid.NewGuid(), null, buffer);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a new <c>RecyclableMemoryStream</c> object with the given tag and with contents copied from the provided
|
||||
/// buffer. The provided buffer is not wrapped or used after construction.
|
||||
/// </summary>
|
||||
/// <remarks>The new stream's position is set to the beginning of the stream when returned.</remarks>
|
||||
/// <param name="id">A unique identifier which can be used to trace usages of the stream</param>
|
||||
/// <param name="tag">A tag which can be used to track the source of the stream</param>
|
||||
/// <param name="buffer">The byte buffer to copy data from</param>
|
||||
/// <returns>A <c>RecyclableMemoryStream</c></returns>
|
||||
public static RecyclableMemoryStream GetStream(Guid id, string tag, ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
RecyclableMemoryStream stream = null;
|
||||
try
|
||||
{
|
||||
stream = new RecyclableMemoryStream(_shared, id, tag, buffer.Length);
|
||||
stream.Write(buffer);
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
catch
|
||||
{
|
||||
stream?.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a new <c>RecyclableMemoryStream</c> object with the given tag and with contents copied from the provided
|
||||
/// buffer. The provided buffer is not wrapped or used after construction.
|
||||
/// </summary>
|
||||
/// <remarks>The new stream's position is set to the beginning of the stream when returned</remarks>
|
||||
/// <param name="id">A unique identifier which can be used to trace usages of the stream</param>
|
||||
/// <param name="tag">A tag which can be used to track the source of the stream</param>
|
||||
/// <param name="buffer">The byte buffer to copy data from</param>
|
||||
/// <param name="offset">The offset from the start of the buffer to copy from</param>
|
||||
/// <param name="count">The number of bytes to copy from the buffer</param>
|
||||
/// <returns>A <c>RecyclableMemoryStream</c></returns>
|
||||
public static RecyclableMemoryStream GetStream(Guid id, string tag, byte[] buffer, int offset, int count)
|
||||
{
|
||||
RecyclableMemoryStream stream = null;
|
||||
try
|
||||
{
|
||||
stream = new RecyclableMemoryStream(_shared, id, tag, count);
|
||||
stream.Write(buffer, offset, count);
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
catch
|
||||
{
|
||||
stream?.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,6 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" />
|
||||
<PackageReference Include="MsgPack.Cli" />
|
||||
<PackageReference Include="System.Management" />
|
||||
</ItemGroup>
|
||||
|
11
Ryujinx.Common/Utilities/CommonJsonContext.cs
Normal file
11
Ryujinx.Common/Utilities/CommonJsonContext.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Utilities
|
||||
{
|
||||
[JsonSerializable(typeof(string[]), TypeInfoPropertyName = "StringArray")]
|
||||
[JsonSerializable(typeof(Dictionary<string, string>), TypeInfoPropertyName = "StringDictionary")]
|
||||
public partial class CommonJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
@@ -1,3 +1,5 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -38,12 +40,7 @@ namespace Ryujinx.Common
|
||||
return null;
|
||||
}
|
||||
|
||||
using (var mem = new MemoryStream())
|
||||
{
|
||||
stream.CopyTo(mem);
|
||||
|
||||
return mem.ToArray();
|
||||
}
|
||||
return StreamUtils.StreamToBytes(stream);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,12 +53,7 @@ namespace Ryujinx.Common
|
||||
return null;
|
||||
}
|
||||
|
||||
using (var mem = new MemoryStream())
|
||||
{
|
||||
await stream.CopyToAsync(mem);
|
||||
|
||||
return mem.ToArray();
|
||||
}
|
||||
return await StreamUtils.StreamToBytesAsync(stream);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,15 +1,62 @@
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace Ryujinx.Common.Utilities
|
||||
{
|
||||
public class JsonHelper
|
||||
{
|
||||
public static JsonNamingPolicy SnakeCase { get; }
|
||||
private static readonly JsonNamingPolicy SnakeCasePolicy = new SnakeCaseNamingPolicy();
|
||||
private const int DefaultFileWriteBufferSize = 4096;
|
||||
|
||||
/// <summary>
|
||||
/// Creates new serializer options with default settings.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It is REQUIRED for you to save returned options statically or as a part of static serializer context
|
||||
/// in order to avoid performance issues. You can safely modify returned options for your case before storing.
|
||||
/// </remarks>
|
||||
public static JsonSerializerOptions GetDefaultSerializerOptions(bool indented = true)
|
||||
{
|
||||
JsonSerializerOptions options = new()
|
||||
{
|
||||
DictionaryKeyPolicy = SnakeCasePolicy,
|
||||
PropertyNamingPolicy = SnakeCasePolicy,
|
||||
WriteIndented = indented,
|
||||
AllowTrailingCommas = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public static string Serialize<T>(T value, JsonTypeInfo<T> typeInfo)
|
||||
{
|
||||
return JsonSerializer.Serialize(value, typeInfo);
|
||||
}
|
||||
|
||||
public static T Deserialize<T>(string value, JsonTypeInfo<T> typeInfo)
|
||||
{
|
||||
return JsonSerializer.Deserialize(value, typeInfo);
|
||||
}
|
||||
|
||||
public static void SerializeToFile<T>(string filePath, T value, JsonTypeInfo<T> typeInfo)
|
||||
{
|
||||
using FileStream file = File.Create(filePath, DefaultFileWriteBufferSize, FileOptions.WriteThrough);
|
||||
JsonSerializer.Serialize(file, value, typeInfo);
|
||||
}
|
||||
|
||||
public static T DeserializeFromFile<T>(string filePath, JsonTypeInfo<T> typeInfo)
|
||||
{
|
||||
using FileStream file = File.OpenRead(filePath);
|
||||
return JsonSerializer.Deserialize(file, typeInfo);
|
||||
}
|
||||
|
||||
public static void SerializeToStream<T>(Stream stream, T value, JsonTypeInfo<T> typeInfo)
|
||||
{
|
||||
JsonSerializer.Serialize(stream, value, typeInfo);
|
||||
}
|
||||
|
||||
private class SnakeCaseNamingPolicy : JsonNamingPolicy
|
||||
{
|
||||
@@ -20,7 +67,7 @@ namespace Ryujinx.Common.Utilities
|
||||
return name;
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
StringBuilder builder = new();
|
||||
|
||||
for (int i = 0; i < name.Length; i++)
|
||||
{
|
||||
@@ -34,7 +81,7 @@ namespace Ryujinx.Common.Utilities
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append("_");
|
||||
builder.Append('_');
|
||||
builder.Append(char.ToLowerInvariant(c));
|
||||
}
|
||||
}
|
||||
@@ -47,64 +94,5 @@ namespace Ryujinx.Common.Utilities
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
static JsonHelper()
|
||||
{
|
||||
SnakeCase = new SnakeCaseNamingPolicy();
|
||||
}
|
||||
|
||||
public static JsonSerializerOptions GetDefaultSerializerOptions(bool prettyPrint = false)
|
||||
{
|
||||
JsonSerializerOptions options = new JsonSerializerOptions
|
||||
{
|
||||
DictionaryKeyPolicy = SnakeCase,
|
||||
PropertyNamingPolicy = SnakeCase,
|
||||
WriteIndented = prettyPrint,
|
||||
AllowTrailingCommas = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip
|
||||
};
|
||||
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
options.Converters.Add(new JsonInputConfigConverter());
|
||||
options.Converters.Add(new JsonMotionConfigControllerConverter());
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public static T Deserialize<T>(Stream stream)
|
||||
{
|
||||
using (BinaryReader reader = new BinaryReader(stream))
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(reader.ReadBytes((int)(stream.Length - stream.Position)), GetDefaultSerializerOptions());
|
||||
}
|
||||
}
|
||||
|
||||
public static T DeserializeFromFile<T>(string path)
|
||||
{
|
||||
return Deserialize<T>(File.ReadAllText(path));
|
||||
}
|
||||
|
||||
public static T Deserialize<T>(string json)
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(json, GetDefaultSerializerOptions());
|
||||
}
|
||||
|
||||
public static void Serialize<TValue>(Stream stream, TValue obj, bool prettyPrint = false)
|
||||
{
|
||||
using (BinaryWriter writer = new BinaryWriter(stream))
|
||||
{
|
||||
writer.Write(SerializeToUtf8Bytes(obj, prettyPrint));
|
||||
}
|
||||
}
|
||||
|
||||
public static string Serialize<TValue>(TValue obj, bool prettyPrint = false)
|
||||
{
|
||||
return JsonSerializer.Serialize(obj, GetDefaultSerializerOptions(prettyPrint));
|
||||
}
|
||||
|
||||
public static byte[] SerializeToUtf8Bytes<T>(T obj, bool prettyPrint = false)
|
||||
{
|
||||
return JsonSerializer.SerializeToUtf8Bytes(obj, GetDefaultSerializerOptions(prettyPrint));
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,8 @@
|
||||
using System.IO;
|
||||
using Microsoft.IO;
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Common.Utilities
|
||||
{
|
||||
@@ -6,12 +10,22 @@ namespace Ryujinx.Common.Utilities
|
||||
{
|
||||
public static byte[] StreamToBytes(Stream input)
|
||||
{
|
||||
using (MemoryStream stream = new MemoryStream())
|
||||
using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
|
||||
{
|
||||
input.CopyTo(stream);
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<byte[]> StreamToBytesAsync(Stream input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
|
||||
{
|
||||
await input.CopyToAsync(stream, cancellationToken);
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
Ryujinx.Common/Utilities/TypedStringEnumConverter.cs
Normal file
34
Ryujinx.Common/Utilities/TypedStringEnumConverter.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies that value of <see cref="TEnum"/> will be serialized as string in JSONs
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Trimming friendly alternative to <see cref="JsonStringEnumConverter"/>.
|
||||
/// Get rid of this converter if dotnet supports similar functionality out of the box.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TEnum">Type of enum to serialize</typeparam>
|
||||
public sealed class TypedStringEnumConverter<TEnum> : JsonConverter<TEnum> where TEnum : struct, Enum
|
||||
{
|
||||
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var enumValue = reader.GetString();
|
||||
if (string.IsNullOrEmpty(enumValue))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return Enum.Parse<TEnum>(enumValue);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,6 @@
|
||||
using Ryujinx.Memory;
|
||||
using Microsoft.IO;
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
@@ -40,7 +42,7 @@ namespace Ryujinx.Cpu
|
||||
|
||||
public static string ReadAsciiString(IVirtualMemoryManager memory, ulong position, long maxSize = -1)
|
||||
{
|
||||
using (MemoryStream ms = new MemoryStream())
|
||||
using (RecyclableMemoryStream ms = MemoryStreamManager.Shared.GetStream())
|
||||
{
|
||||
for (long offs = 0; offs < maxSize || maxSize == -1; offs++)
|
||||
{
|
||||
@@ -54,7 +56,7 @@ namespace Ryujinx.Cpu
|
||||
ms.WriteByte(value);
|
||||
}
|
||||
|
||||
return Encoding.ASCII.GetString(ms.ToArray());
|
||||
return Encoding.ASCII.GetString(ms.GetReadOnlySequence());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -28,5 +28,10 @@ namespace Ryujinx.Cpu.Tracking
|
||||
public void Reprotect(bool asDirty = false) => _impl.Reprotect(asDirty);
|
||||
|
||||
public bool OverlapsWith(ulong address, ulong size) => _impl.OverlapsWith(address, size);
|
||||
|
||||
public bool RangeEquals(CpuRegionHandle other)
|
||||
{
|
||||
return _impl.RealAddress == other._impl.RealAddress && _impl.RealSize == other._impl.RealSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -15,7 +15,12 @@ namespace Ryujinx.Graphics.GAL
|
||||
|
||||
void BackgroundContextAction(Action action, bool alwaysBackground = false);
|
||||
|
||||
BufferHandle CreateBuffer(int size);
|
||||
BufferHandle CreateBuffer(int size, BufferHandle storageHint);
|
||||
|
||||
BufferHandle CreateBuffer(int size)
|
||||
{
|
||||
return CreateBuffer(size, BufferHandle.Null);
|
||||
}
|
||||
|
||||
IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info);
|
||||
|
||||
@@ -26,7 +31,7 @@ namespace Ryujinx.Graphics.GAL
|
||||
|
||||
void DeleteBuffer(BufferHandle buffer);
|
||||
|
||||
ReadOnlySpan<byte> GetBufferData(BufferHandle buffer, int offset, int size);
|
||||
PinnedSpan<byte> GetBufferData(BufferHandle buffer, int offset, int size);
|
||||
|
||||
Capabilities GetCapabilities();
|
||||
ulong GetCurrentSync();
|
||||
|
@@ -15,8 +15,8 @@ namespace Ryujinx.Graphics.GAL
|
||||
|
||||
ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel);
|
||||
|
||||
ReadOnlySpan<byte> GetData();
|
||||
ReadOnlySpan<byte> GetData(int layer, int level);
|
||||
PinnedSpan<byte> GetData();
|
||||
PinnedSpan<byte> GetData(int layer, int level);
|
||||
|
||||
void SetData(SpanOrArray<byte> data);
|
||||
void SetData(SpanOrArray<byte> data, int layer, int level);
|
||||
|
@@ -21,9 +21,9 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Buffer
|
||||
|
||||
public static void Run(ref BufferGetDataCommand command, ThreadedRenderer threaded, IRenderer renderer)
|
||||
{
|
||||
ReadOnlySpan<byte> result = renderer.GetBufferData(threaded.Buffers.MapBuffer(command._buffer), command._offset, command._size);
|
||||
PinnedSpan<byte> result = renderer.GetBufferData(threaded.Buffers.MapBuffer(command._buffer), command._offset, command._size);
|
||||
|
||||
command._result.Get(threaded).Result = new PinnedSpan<byte>(result);
|
||||
command._result.Get(threaded).Result = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,16 +5,25 @@
|
||||
public CommandType CommandType => CommandType.CreateBuffer;
|
||||
private BufferHandle _threadedHandle;
|
||||
private int _size;
|
||||
private BufferHandle _storageHint;
|
||||
|
||||
public void Set(BufferHandle threadedHandle, int size)
|
||||
public void Set(BufferHandle threadedHandle, int size, BufferHandle storageHint)
|
||||
{
|
||||
_threadedHandle = threadedHandle;
|
||||
_size = size;
|
||||
_storageHint = storageHint;
|
||||
}
|
||||
|
||||
public static void Run(ref CreateBufferCommand command, ThreadedRenderer threaded, IRenderer renderer)
|
||||
{
|
||||
threaded.Buffers.AssignBuffer(command._threadedHandle, renderer.CreateBuffer(command._size));
|
||||
BufferHandle hint = BufferHandle.Null;
|
||||
|
||||
if (command._storageHint != BufferHandle.Null)
|
||||
{
|
||||
hint = threaded.Buffers.MapBuffer(command._storageHint);
|
||||
}
|
||||
|
||||
threaded.Buffers.AssignBuffer(command._threadedHandle, renderer.CreateBuffer(command._size, hint));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -18,9 +18,9 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Texture
|
||||
|
||||
public static void Run(ref TextureGetDataCommand command, ThreadedRenderer threaded, IRenderer renderer)
|
||||
{
|
||||
ReadOnlySpan<byte> result = command._texture.Get(threaded).Base.GetData();
|
||||
PinnedSpan<byte> result = command._texture.Get(threaded).Base.GetData();
|
||||
|
||||
command._result.Get(threaded).Result = new PinnedSpan<byte>(result);
|
||||
command._result.Get(threaded).Result = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -22,9 +22,9 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Texture
|
||||
|
||||
public static void Run(ref TextureGetDataSliceCommand command, ThreadedRenderer threaded, IRenderer renderer)
|
||||
{
|
||||
ReadOnlySpan<byte> result = command._texture.Get(threaded).Base.GetData(command._layer, command._level);
|
||||
PinnedSpan<byte> result = command._texture.Get(threaded).Base.GetData(command._layer, command._level);
|
||||
|
||||
command._result.Get(threaded).Result = new PinnedSpan<byte>(result);
|
||||
command._result.Get(threaded).Result = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,23 +0,0 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Graphics.GAL.Multithreading.Model
|
||||
{
|
||||
unsafe struct PinnedSpan<T> where T : unmanaged
|
||||
{
|
||||
private void* _ptr;
|
||||
private int _size;
|
||||
|
||||
public PinnedSpan(ReadOnlySpan<T> span)
|
||||
{
|
||||
_ptr = Unsafe.AsPointer(ref MemoryMarshal.GetReference(span));
|
||||
_size = span.Length;
|
||||
}
|
||||
|
||||
public ReadOnlySpan<T> Get()
|
||||
{
|
||||
return new ReadOnlySpan<T>(_ptr, _size * Unsafe.SizeOf<T>());
|
||||
}
|
||||
}
|
||||
}
|
@@ -72,7 +72,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Resources
|
||||
return newTex;
|
||||
}
|
||||
|
||||
public ReadOnlySpan<byte> GetData()
|
||||
public PinnedSpan<byte> GetData()
|
||||
{
|
||||
if (_renderer.IsGpuThread())
|
||||
{
|
||||
@@ -80,7 +80,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Resources
|
||||
_renderer.New<TextureGetDataCommand>().Set(Ref(this), Ref(box));
|
||||
_renderer.InvokeCommand();
|
||||
|
||||
return box.Result.Get();
|
||||
return box.Result;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -90,7 +90,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Resources
|
||||
}
|
||||
}
|
||||
|
||||
public ReadOnlySpan<byte> GetData(int layer, int level)
|
||||
public PinnedSpan<byte> GetData(int layer, int level)
|
||||
{
|
||||
if (_renderer.IsGpuThread())
|
||||
{
|
||||
@@ -98,7 +98,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Resources
|
||||
_renderer.New<TextureGetDataSliceCommand>().Set(Ref(this), Ref(box), layer, level);
|
||||
_renderer.InvokeCommand();
|
||||
|
||||
return box.Result.Get();
|
||||
return box.Result;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@@ -265,10 +265,10 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
}
|
||||
}
|
||||
|
||||
public BufferHandle CreateBuffer(int size)
|
||||
public BufferHandle CreateBuffer(int size, BufferHandle storageHint)
|
||||
{
|
||||
BufferHandle handle = Buffers.CreateBufferHandle();
|
||||
New<CreateBufferCommand>().Set(handle, size);
|
||||
New<CreateBufferCommand>().Set(handle, size, storageHint);
|
||||
QueueCommand();
|
||||
|
||||
return handle;
|
||||
@@ -329,7 +329,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
QueueCommand();
|
||||
}
|
||||
|
||||
public ReadOnlySpan<byte> GetBufferData(BufferHandle buffer, int offset, int size)
|
||||
public PinnedSpan<byte> GetBufferData(BufferHandle buffer, int offset, int size)
|
||||
{
|
||||
if (IsGpuThread())
|
||||
{
|
||||
@@ -337,7 +337,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
||||
New<BufferGetDataCommand>().Set(buffer, offset, size, Ref(box));
|
||||
InvokeCommand();
|
||||
|
||||
return box.Result.Get();
|
||||
return box.Result;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
53
Ryujinx.Graphics.GAL/PinnedSpan.cs
Normal file
53
Ryujinx.Graphics.GAL/PinnedSpan.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Graphics.GAL
|
||||
{
|
||||
public unsafe struct PinnedSpan<T> : IDisposable where T : unmanaged
|
||||
{
|
||||
private void* _ptr;
|
||||
private int _size;
|
||||
private Action _disposeAction;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new PinnedSpan from an existing ReadOnlySpan. The span *must* be pinned in memory.
|
||||
/// The data must be guaranteed to live until disposeAction is called.
|
||||
/// </summary>
|
||||
/// <param name="span">Existing span</param>
|
||||
/// <param name="disposeAction">Action to call on dispose</param>
|
||||
/// <remarks>
|
||||
/// If a dispose action is not provided, it is safe to assume the resource will be available until the next call.
|
||||
/// </remarks>
|
||||
public static PinnedSpan<T> UnsafeFromSpan(ReadOnlySpan<T> span, Action disposeAction = null)
|
||||
{
|
||||
return new PinnedSpan<T>(Unsafe.AsPointer(ref MemoryMarshal.GetReference(span)), span.Length, disposeAction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new PinnedSpan from an existing unsafe region. The data must be guaranteed to live until disposeAction is called.
|
||||
/// </summary>
|
||||
/// <param name="ptr">Pointer to the region</param>
|
||||
/// <param name="size">The total items of T the region contains</param>
|
||||
/// <param name="disposeAction">Action to call on dispose</param>
|
||||
/// <remarks>
|
||||
/// If a dispose action is not provided, it is safe to assume the resource will be available until the next call.
|
||||
/// </remarks>
|
||||
public PinnedSpan(void* ptr, int size, Action disposeAction = null)
|
||||
{
|
||||
_ptr = ptr;
|
||||
_size = size;
|
||||
_disposeAction = disposeAction;
|
||||
}
|
||||
|
||||
public ReadOnlySpan<T> Get()
|
||||
{
|
||||
return new ReadOnlySpan<T>(_ptr, _size * Unsafe.SizeOf<T>());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposeAction?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
@@ -20,5 +20,15 @@ namespace Ryujinx.Graphics.GAL
|
||||
{
|
||||
return target == Target.Texture2DMultisample || target == Target.Texture2DMultisampleArray;
|
||||
}
|
||||
|
||||
public static bool HasDepthOrLayers(this Target target)
|
||||
{
|
||||
return target == Target.Texture3D ||
|
||||
target == Target.Texture1DArray ||
|
||||
target == Target.Texture2DArray ||
|
||||
target == Target.Texture2DMultisampleArray ||
|
||||
target == Target.Cubemap ||
|
||||
target == Target.CubemapArray;
|
||||
}
|
||||
}
|
||||
}
|
@@ -152,21 +152,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||
|
||||
ulong ticks = _context.GetTimestamp();
|
||||
|
||||
float divisor = type switch
|
||||
{
|
||||
ReportCounterType.SamplesPassed => _channel.TextureManager.RenderTargetScale * _channel.TextureManager.RenderTargetScale,
|
||||
_ => 1f
|
||||
};
|
||||
|
||||
ICounterEvent counter = null;
|
||||
|
||||
void resultHandler(object evt, ulong result)
|
||||
{
|
||||
if (divisor != 1f)
|
||||
{
|
||||
result = (ulong)MathF.Ceiling(result / divisor);
|
||||
}
|
||||
|
||||
CounterData counterData = new CounterData
|
||||
{
|
||||
Counter = result,
|
||||
|
@@ -36,6 +36,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
public TexturePool Pool;
|
||||
public int ID;
|
||||
public ulong GpuAddress;
|
||||
}
|
||||
|
||||
private GpuContext _context;
|
||||
@@ -162,6 +163,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
public bool IsView => _viewStorage != this;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not this texture has views.
|
||||
/// </summary>
|
||||
public bool HasViews => _views.Count > 0;
|
||||
|
||||
private int _referenceCount;
|
||||
private List<TexturePoolOwner> _poolOwners;
|
||||
|
||||
@@ -354,7 +360,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
|
||||
texture._viewStorage = this;
|
||||
|
||||
Group.UpdateViews(_views);
|
||||
Group.UpdateViews(_views, texture);
|
||||
|
||||
if (texture.Group != null && texture.Group != Group)
|
||||
{
|
||||
@@ -378,11 +384,24 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
_views.Remove(texture);
|
||||
|
||||
Group.RemoveView(texture);
|
||||
|
||||
texture._viewStorage = texture;
|
||||
|
||||
DecrementReferenceCount();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the texture's physical memory range. This forces tracking to regenerate.
|
||||
/// </summary>
|
||||
/// <param name="range">New physical memory range backing the texture</param>
|
||||
public void ReplaceRange(MultiRange range)
|
||||
{
|
||||
Range = range;
|
||||
|
||||
Group.RangeChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy dependency to a texture that is view compatible with this one.
|
||||
/// When either texture is modified, the texture data will be copied to the other to keep them in sync.
|
||||
@@ -715,6 +734,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
height = Math.Max(height >> level, 1);
|
||||
depth = Math.Max(depth >> level, 1);
|
||||
|
||||
int sliceDepth = single ? 1 : depth;
|
||||
|
||||
SpanOrArray<byte> result;
|
||||
|
||||
if (Info.IsLinear)
|
||||
@@ -735,7 +756,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
width,
|
||||
height,
|
||||
depth,
|
||||
single ? 1 : depth,
|
||||
sliceDepth,
|
||||
levels,
|
||||
layers,
|
||||
Info.FormatInfo.BlockWidth,
|
||||
@@ -759,7 +780,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
Info.FormatInfo.BlockHeight,
|
||||
width,
|
||||
height,
|
||||
depth,
|
||||
sliceDepth,
|
||||
levels,
|
||||
layers,
|
||||
out byte[] decoded))
|
||||
@@ -771,7 +792,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
|
||||
if (GraphicsConfig.EnableTextureRecompression)
|
||||
{
|
||||
decoded = BCnEncoder.EncodeBC7(decoded, width, height, depth, levels, layers);
|
||||
decoded = BCnEncoder.EncodeBC7(decoded, width, height, sliceDepth, levels, layers);
|
||||
}
|
||||
|
||||
result = decoded;
|
||||
@@ -782,15 +803,15 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
case Format.Etc2RgbaSrgb:
|
||||
case Format.Etc2RgbaUnorm:
|
||||
result = ETC2Decoder.DecodeRgba(result, width, height, depth, levels, layers);
|
||||
result = ETC2Decoder.DecodeRgba(result, width, height, sliceDepth, levels, layers);
|
||||
break;
|
||||
case Format.Etc2RgbPtaSrgb:
|
||||
case Format.Etc2RgbPtaUnorm:
|
||||
result = ETC2Decoder.DecodePta(result, width, height, depth, levels, layers);
|
||||
result = ETC2Decoder.DecodePta(result, width, height, sliceDepth, levels, layers);
|
||||
break;
|
||||
case Format.Etc2RgbSrgb:
|
||||
case Format.Etc2RgbUnorm:
|
||||
result = ETC2Decoder.DecodeRgb(result, width, height, depth, levels, layers);
|
||||
result = ETC2Decoder.DecodeRgb(result, width, height, sliceDepth, levels, layers);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -800,31 +821,31 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
case Format.Bc1RgbaSrgb:
|
||||
case Format.Bc1RgbaUnorm:
|
||||
result = BCnDecoder.DecodeBC1(result, width, height, depth, levels, layers);
|
||||
result = BCnDecoder.DecodeBC1(result, width, height, sliceDepth, levels, layers);
|
||||
break;
|
||||
case Format.Bc2Srgb:
|
||||
case Format.Bc2Unorm:
|
||||
result = BCnDecoder.DecodeBC2(result, width, height, depth, levels, layers);
|
||||
result = BCnDecoder.DecodeBC2(result, width, height, sliceDepth, levels, layers);
|
||||
break;
|
||||
case Format.Bc3Srgb:
|
||||
case Format.Bc3Unorm:
|
||||
result = BCnDecoder.DecodeBC3(result, width, height, depth, levels, layers);
|
||||
result = BCnDecoder.DecodeBC3(result, width, height, sliceDepth, levels, layers);
|
||||
break;
|
||||
case Format.Bc4Snorm:
|
||||
case Format.Bc4Unorm:
|
||||
result = BCnDecoder.DecodeBC4(result, width, height, depth, levels, layers, Format == Format.Bc4Snorm);
|
||||
result = BCnDecoder.DecodeBC4(result, width, height, sliceDepth, levels, layers, Format == Format.Bc4Snorm);
|
||||
break;
|
||||
case Format.Bc5Snorm:
|
||||
case Format.Bc5Unorm:
|
||||
result = BCnDecoder.DecodeBC5(result, width, height, depth, levels, layers, Format == Format.Bc5Snorm);
|
||||
result = BCnDecoder.DecodeBC5(result, width, height, sliceDepth, levels, layers, Format == Format.Bc5Snorm);
|
||||
break;
|
||||
case Format.Bc6HSfloat:
|
||||
case Format.Bc6HUfloat:
|
||||
result = BCnDecoder.DecodeBC6(result, width, height, depth, levels, layers, Format == Format.Bc6HSfloat);
|
||||
result = BCnDecoder.DecodeBC6(result, width, height, sliceDepth, levels, layers, Format == Format.Bc6HSfloat);
|
||||
break;
|
||||
case Format.Bc7Srgb:
|
||||
case Format.Bc7Unorm:
|
||||
result = BCnDecoder.DecodeBC7(result, width, height, depth, levels, layers);
|
||||
result = BCnDecoder.DecodeBC7(result, width, height, sliceDepth, levels, layers);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1001,13 +1022,12 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// This method should be used to retrieve data that was modified by the host GPU.
|
||||
/// This is not cheap, avoid doing that unless strictly needed.
|
||||
/// </remarks>
|
||||
/// <param name="output">An output span to place the texture data into. If empty, one is generated</param>
|
||||
/// <param name="output">An output span to place the texture data into</param>
|
||||
/// <param name="blacklist">True if the texture should be blacklisted, false otherwise</param>
|
||||
/// <param name="texture">The specific host texture to flush. Defaults to this texture</param>
|
||||
/// <returns>The span containing the texture data</returns>
|
||||
private ReadOnlySpan<byte> GetTextureDataFromGpu(Span<byte> output, bool blacklist, ITexture texture = null)
|
||||
private void GetTextureDataFromGpu(Span<byte> output, bool blacklist, ITexture texture = null)
|
||||
{
|
||||
ReadOnlySpan<byte> data;
|
||||
PinnedSpan<byte> data;
|
||||
|
||||
if (texture != null)
|
||||
{
|
||||
@@ -1033,9 +1053,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
}
|
||||
}
|
||||
|
||||
data = ConvertFromHostCompatibleFormat(output, data);
|
||||
ConvertFromHostCompatibleFormat(output, data.Get());
|
||||
|
||||
return data;
|
||||
data.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1050,10 +1070,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <param name="level">The level of the texture to flush</param>
|
||||
/// <param name="blacklist">True if the texture should be blacklisted, false otherwise</param>
|
||||
/// <param name="texture">The specific host texture to flush. Defaults to this texture</param>
|
||||
/// <returns>The span containing the texture data</returns>
|
||||
public ReadOnlySpan<byte> GetTextureDataSliceFromGpu(Span<byte> output, int layer, int level, bool blacklist, ITexture texture = null)
|
||||
public void GetTextureDataSliceFromGpu(Span<byte> output, int layer, int level, bool blacklist, ITexture texture = null)
|
||||
{
|
||||
ReadOnlySpan<byte> data;
|
||||
PinnedSpan<byte> data;
|
||||
|
||||
if (texture != null)
|
||||
{
|
||||
@@ -1079,9 +1098,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
}
|
||||
}
|
||||
|
||||
data = ConvertFromHostCompatibleFormat(output, data, level, true);
|
||||
ConvertFromHostCompatibleFormat(output, data.Get(), level, true);
|
||||
|
||||
return data;
|
||||
data.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1454,8 +1473,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
|
||||
MultiRange otherRange = texture.Range;
|
||||
|
||||
IEnumerable<MultiRange> regions = _sizeInfo.AllRegions().Select((region) => Range.GetSlice((ulong)region.Offset, (ulong)region.Size));
|
||||
IEnumerable<MultiRange> otherRegions = texture._sizeInfo.AllRegions().Select((region) => otherRange.GetSlice((ulong)region.Offset, (ulong)region.Size));
|
||||
IEnumerable<MultiRange> regions = _sizeInfo.AllRegions().Select((region) => Range.Slice((ulong)region.Offset, (ulong)region.Size));
|
||||
IEnumerable<MultiRange> otherRegions = texture._sizeInfo.AllRegions().Select((region) => otherRange.Slice((ulong)region.Offset, (ulong)region.Size));
|
||||
|
||||
foreach (MultiRange region in regions)
|
||||
{
|
||||
@@ -1484,11 +1503,12 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
/// <param name="pool">The texture pool this texture has been added to</param>
|
||||
/// <param name="id">The ID of the reference to this texture in the pool</param>
|
||||
public void IncrementReferenceCount(TexturePool pool, int id)
|
||||
/// <param name="gpuVa">GPU VA of the pool reference</param>
|
||||
public void IncrementReferenceCount(TexturePool pool, int id, ulong gpuVa)
|
||||
{
|
||||
lock (_poolOwners)
|
||||
{
|
||||
_poolOwners.Add(new TexturePoolOwner { Pool = pool, ID = id });
|
||||
_poolOwners.Add(new TexturePoolOwner { Pool = pool, ID = id, GpuAddress = gpuVa });
|
||||
}
|
||||
_referenceCount++;
|
||||
|
||||
@@ -1585,6 +1605,36 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
InvalidatedSequence++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue updating texture mappings on the pool. Happens from another thread.
|
||||
/// </summary>
|
||||
public void UpdatePoolMappings()
|
||||
{
|
||||
lock (_poolOwners)
|
||||
{
|
||||
ulong address = 0;
|
||||
|
||||
foreach (var owner in _poolOwners)
|
||||
{
|
||||
if (address == 0 || address == owner.GpuAddress)
|
||||
{
|
||||
address = owner.GpuAddress;
|
||||
|
||||
owner.Pool.QueueUpdateMapping(this, owner.ID);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If there is a different GPU VA mapping, prefer the first and delete the others.
|
||||
owner.Pool.ForceRemove(this, owner.ID, true);
|
||||
}
|
||||
}
|
||||
|
||||
_poolOwners.Clear();
|
||||
}
|
||||
|
||||
InvalidatedSequence++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete the texture if it is not used anymore.
|
||||
/// The texture is considered unused when the reference count is zero,
|
||||
@@ -1636,7 +1686,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
Group.ClearModified(unmapRange);
|
||||
}
|
||||
|
||||
RemoveFromPools(true);
|
||||
UpdatePoolMappings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@@ -194,6 +194,39 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
_cache.Lift(texture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to update a texture's physical memory range.
|
||||
/// Returns false if there is an existing texture that matches with the updated range.
|
||||
/// </summary>
|
||||
/// <param name="texture">Texture to update</param>
|
||||
/// <param name="range">New physical memory range</param>
|
||||
/// <returns>True if the mapping was updated, false otherwise</returns>
|
||||
public bool UpdateMapping(Texture texture, MultiRange range)
|
||||
{
|
||||
// There cannot be an existing texture compatible with this mapping in the texture cache already.
|
||||
int overlapCount = _textures.FindOverlaps(range, ref _textureOverlaps);
|
||||
|
||||
for (int i = 0; i < overlapCount; i++)
|
||||
{
|
||||
var other = _textureOverlaps[i];
|
||||
|
||||
if (texture != other &&
|
||||
(texture.IsViewCompatible(other.Info, other.Range, true, other.LayerSize, _context.Capabilities, out _, out _) != TextureViewCompatibility.Incompatible ||
|
||||
other.IsViewCompatible(texture.Info, texture.Range, true, texture.LayerSize, _context.Capabilities, out _, out _) != TextureViewCompatibility.Incompatible))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_textures.Remove(texture);
|
||||
|
||||
texture.ReplaceRange(range);
|
||||
|
||||
_textures.Add(texture);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find an existing texture, or create a new one if not found.
|
||||
/// </summary>
|
||||
|
@@ -39,6 +39,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
class TextureGroup : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Threshold of layers to force granular handles (and thus partial loading) on array/3D textures.
|
||||
/// </summary>
|
||||
private const int GranularLayerThreshold = 8;
|
||||
|
||||
private delegate void HandlesCallbackDelegate(int baseHandle, int regionCount, bool split = false);
|
||||
|
||||
/// <summary>
|
||||
@@ -116,8 +121,30 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
_allOffsets = size.AllOffsets;
|
||||
_sliceSizes = size.SliceSizes;
|
||||
|
||||
if (Storage.Target.HasDepthOrLayers() && Storage.Info.GetSlices() > GranularLayerThreshold)
|
||||
{
|
||||
_hasLayerViews = true;
|
||||
_hasMipViews = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
(_hasLayerViews, _hasMipViews) = PropagateGranularity(hasLayerViews, hasMipViews);
|
||||
|
||||
// If the texture is partially mapped, fully subdivide handles immediately.
|
||||
|
||||
MultiRange range = Storage.Range;
|
||||
for (int i = 0; i < range.Count; i++)
|
||||
{
|
||||
if (range.GetSubRange(i).Address == MemoryManager.PteUnmapped)
|
||||
{
|
||||
_hasLayerViews = true;
|
||||
_hasMipViews = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RecalculateHandleRegions();
|
||||
}
|
||||
|
||||
@@ -249,7 +276,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
bool dirty = false;
|
||||
bool anyModified = false;
|
||||
bool anyUnmapped = false;
|
||||
bool anyNotDirty = false;
|
||||
|
||||
for (int i = 0; i < regionCount; i++)
|
||||
{
|
||||
@@ -294,20 +321,21 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
dirty |= handleDirty;
|
||||
}
|
||||
|
||||
anyUnmapped |= handleUnmapped;
|
||||
|
||||
if (group.NeedsCopy)
|
||||
{
|
||||
// The texture we copied from is still being written to. Copy from it again the next time this texture is used.
|
||||
texture.SignalGroupDirty();
|
||||
}
|
||||
|
||||
_loadNeeded[baseHandle + i] = handleDirty && !handleUnmapped;
|
||||
bool loadNeeded = handleDirty && !handleUnmapped;
|
||||
|
||||
anyNotDirty |= !loadNeeded;
|
||||
_loadNeeded[baseHandle + i] = loadNeeded;
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
{
|
||||
if (anyUnmapped || (_handles.Length > 1 && (anyModified || split)))
|
||||
if (anyNotDirty || (_handles.Length > 1 && (anyModified || split)))
|
||||
{
|
||||
// Partial texture invalidation. Only update the layers/levels with dirty flags of the storage.
|
||||
|
||||
@@ -331,24 +359,56 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <param name="regionCount">The number of handles to synchronize</param>
|
||||
private void SynchronizePartial(int baseHandle, int regionCount)
|
||||
{
|
||||
int spanEndIndex = -1;
|
||||
int spanBase = 0;
|
||||
ReadOnlySpan<byte> dataSpan = ReadOnlySpan<byte>.Empty;
|
||||
|
||||
for (int i = 0; i < regionCount; i++)
|
||||
{
|
||||
if (_loadNeeded[baseHandle + i])
|
||||
{
|
||||
var info = GetHandleInformation(baseHandle + i);
|
||||
|
||||
// Ensure the data for this handle is loaded in the span.
|
||||
if (spanEndIndex <= i - 1)
|
||||
{
|
||||
spanEndIndex = i;
|
||||
|
||||
if (_is3D)
|
||||
{
|
||||
// Look ahead to see how many handles need to be loaded.
|
||||
for (int j = i + 1; j < regionCount; j++)
|
||||
{
|
||||
if (_loadNeeded[baseHandle + j])
|
||||
{
|
||||
spanEndIndex = j;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var endInfo = spanEndIndex == i ? info : GetHandleInformation(baseHandle + spanEndIndex);
|
||||
|
||||
spanBase = _allOffsets[info.Index];
|
||||
int spanLast = _allOffsets[endInfo.Index + endInfo.Layers * endInfo.Levels - 1];
|
||||
int endOffset = Math.Min(spanLast + _sliceSizes[endInfo.BaseLevel + endInfo.Levels - 1], (int)Storage.Size);
|
||||
int size = endOffset - spanBase;
|
||||
|
||||
dataSpan = _physicalMemory.GetSpan(Storage.Range.Slice((ulong)spanBase, (ulong)size));
|
||||
}
|
||||
|
||||
// Only one of these will be greater than 1, as partial sync is only called when there are sub-image views.
|
||||
for (int layer = 0; layer < info.Layers; layer++)
|
||||
{
|
||||
for (int level = 0; level < info.Levels; level++)
|
||||
{
|
||||
int offsetIndex = GetOffsetIndex(info.BaseLayer + layer, info.BaseLevel + level);
|
||||
|
||||
int offset = _allOffsets[offsetIndex];
|
||||
int endOffset = Math.Min(offset + _sliceSizes[info.BaseLevel + level], (int)Storage.Size);
|
||||
int size = endOffset - offset;
|
||||
|
||||
ReadOnlySpan<byte> data = _physicalMemory.GetSpan(Storage.Range.GetSlice((ulong)offset, (ulong)size));
|
||||
ReadOnlySpan<byte> data = dataSpan.Slice(offset - spanBase);
|
||||
|
||||
SpanOrArray<byte> result = Storage.ConvertToHostCompatibleFormat(data, info.BaseLevel + level, true);
|
||||
|
||||
@@ -413,7 +473,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
int endOffset = Math.Min(offset + _sliceSizes[level], (int)Storage.Size);
|
||||
int size = endOffset - offset;
|
||||
|
||||
using WritableRegion region = _physicalMemory.GetWritableRegion(Storage.Range.GetSlice((ulong)offset, (ulong)size), tracked);
|
||||
using WritableRegion region = _physicalMemory.GetWritableRegion(Storage.Range.Slice((ulong)offset, (ulong)size), tracked);
|
||||
|
||||
Storage.GetTextureDataSliceFromGpu(region.Memory.Span, layer, level, tracked, texture);
|
||||
}
|
||||
@@ -865,8 +925,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <returns>A TextureGroupHandle covering the given views</returns>
|
||||
private TextureGroupHandle GenerateHandles(int viewStart, int views)
|
||||
{
|
||||
int viewEnd = viewStart + views - 1;
|
||||
(_, int lastLevel) = GetLayerLevelForView(viewEnd);
|
||||
|
||||
int offset = _allOffsets[viewStart];
|
||||
int endOffset = (viewStart + views == _allOffsets.Length) ? (int)Storage.Size : _allOffsets[viewStart + views];
|
||||
int endOffset = _allOffsets[viewEnd] + _sliceSizes[lastLevel];
|
||||
int size = endOffset - offset;
|
||||
|
||||
var result = new List<CpuRegionHandle>();
|
||||
@@ -926,7 +989,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// Update the views in this texture group, rebuilding the memory tracking if required.
|
||||
/// </summary>
|
||||
/// <param name="views">The views list of the storage texture</param>
|
||||
public void UpdateViews(List<Texture> views)
|
||||
/// <param name="texture">The texture that has been added, if that is the only change, otherwise null</param>
|
||||
public void UpdateViews(List<Texture> views, Texture texture)
|
||||
{
|
||||
// This is saved to calculate overlapping views for each handle.
|
||||
_views = views;
|
||||
@@ -963,6 +1027,17 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
}
|
||||
|
||||
if (!regionsRebuilt)
|
||||
{
|
||||
if (texture != null)
|
||||
{
|
||||
int offset = FindOffset(texture);
|
||||
|
||||
foreach (TextureGroupHandle handle in _handles)
|
||||
{
|
||||
handle.AddOverlap(offset, texture);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Must update the overlapping views on all handles, but only if they were not just recreated.
|
||||
|
||||
@@ -971,10 +1046,26 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
handle.RecalculateOverlaps(this, views);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SignalAllDirty();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Removes a view from the group, removing it from all overlap lists.
|
||||
/// </summary>
|
||||
/// <param name="view">View to remove from the group</param>
|
||||
public void RemoveView(Texture view)
|
||||
{
|
||||
int offset = FindOffset(view);
|
||||
|
||||
foreach (TextureGroupHandle handle in _handles)
|
||||
{
|
||||
handle.RemoveOverlap(offset, view);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inherit handle state from an old set of handles, such as modified and dirty flags.
|
||||
/// </summary>
|
||||
@@ -1057,19 +1148,61 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// The dirty flags from the previous handles will be kept.
|
||||
/// </summary>
|
||||
/// <param name="handles">The handles to replace the current handles with</param>
|
||||
private void ReplaceHandles(TextureGroupHandle[] handles)
|
||||
/// <param name="rangeChanged">True if the storage memory range changed since the last region handle generation</param>
|
||||
private void ReplaceHandles(TextureGroupHandle[] handles, bool rangeChanged)
|
||||
{
|
||||
if (_handles != null)
|
||||
{
|
||||
// When replacing handles, they should start as non-dirty.
|
||||
|
||||
foreach (TextureGroupHandle groupHandle in handles)
|
||||
{
|
||||
if (rangeChanged)
|
||||
{
|
||||
// When the storage range changes, this becomes a little different.
|
||||
// If a range does not match one in the original, treat it as modified.
|
||||
// It has been newly mapped and its data must be synchronized.
|
||||
|
||||
if (groupHandle.Handles.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var oldGroup in _handles)
|
||||
{
|
||||
if (!groupHandle.OverlapsWith(oldGroup.Offset, oldGroup.Size))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (CpuRegionHandle handle in groupHandle.Handles)
|
||||
{
|
||||
bool hasMatch = false;
|
||||
|
||||
foreach (var oldHandle in oldGroup.Handles)
|
||||
{
|
||||
if (oldHandle.RangeEquals(handle))
|
||||
{
|
||||
hasMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMatch)
|
||||
{
|
||||
handle.Reprotect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (CpuRegionHandle handle in groupHandle.Handles)
|
||||
{
|
||||
handle.Reprotect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InheritHandles(_handles, handles, 0);
|
||||
|
||||
@@ -1089,7 +1222,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <summary>
|
||||
/// Recalculate handle regions for this texture group, and inherit existing state into the new handles.
|
||||
/// </summary>
|
||||
private void RecalculateHandleRegions()
|
||||
/// <param name="rangeChanged">True if the storage memory range changed since the last region handle generation</param>
|
||||
private void RecalculateHandleRegions(bool rangeChanged = false)
|
||||
{
|
||||
TextureGroupHandle[] handles;
|
||||
|
||||
@@ -1171,7 +1305,21 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
}
|
||||
}
|
||||
|
||||
ReplaceHandles(handles);
|
||||
ReplaceHandles(handles, rangeChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regenerates handles when the storage range has been remapped.
|
||||
/// This forces the regions to be fully subdivided.
|
||||
/// </summary>
|
||||
public void RangeChanged()
|
||||
{
|
||||
_hasLayerViews = true;
|
||||
_hasMipViews = true;
|
||||
|
||||
RecalculateHandleRegions(true);
|
||||
|
||||
SignalAllDirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1271,13 +1419,13 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
for (int i = 0; i < _allOffsets.Length; i++)
|
||||
{
|
||||
(int layer, int level) = GetLayerLevelForView(i);
|
||||
MultiRange handleRange = Storage.Range.GetSlice((ulong)_allOffsets[i], 1);
|
||||
MultiRange handleRange = Storage.Range.Slice((ulong)_allOffsets[i], 1);
|
||||
ulong handleBase = handleRange.GetSubRange(0).Address;
|
||||
|
||||
for (int j = 0; j < other._handles.Length; j++)
|
||||
{
|
||||
(int otherLayer, int otherLevel) = other.GetLayerLevelForView(j);
|
||||
MultiRange otherHandleRange = other.Storage.Range.GetSlice((ulong)other._allOffsets[j], 1);
|
||||
MultiRange otherHandleRange = other.Storage.Range.Slice((ulong)other._allOffsets[j], 1);
|
||||
ulong otherHandleBase = otherHandleRange.GetSubRange(0).Address;
|
||||
|
||||
if (handleBase == otherHandleBase)
|
||||
@@ -1354,7 +1502,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
// Handles list is not modified by another thread, only replaced, so this is thread safe.
|
||||
// Remove modified flags from all overlapping handles, so that the textures don't flush to unmapped/remapped GPU memory.
|
||||
|
||||
MultiRange subRange = Storage.Range.GetSlice((ulong)handle.Offset, (ulong)handle.Size);
|
||||
MultiRange subRange = Storage.Range.Slice((ulong)handle.Offset, (ulong)handle.Size);
|
||||
|
||||
if (range.OverlapsWith(subRange))
|
||||
{
|
||||
|
@@ -159,6 +159,42 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a single texture view as an overlap if its range overlaps.
|
||||
/// </summary>
|
||||
/// <param name="offset">The offset of the view in the group</param>
|
||||
/// <param name="view">The texture to add as an overlap</param>
|
||||
public void AddOverlap(int offset, Texture view)
|
||||
{
|
||||
// Overlaps can be accessed from the memory tracking signal handler, so access must be atomic.
|
||||
|
||||
if (OverlapsWith(offset, (int)view.Size))
|
||||
{
|
||||
lock (Overlaps)
|
||||
{
|
||||
Overlaps.Add(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a single texture view as an overlap if its range overlaps.
|
||||
/// </summary>
|
||||
/// <param name="offset">The offset of the view in the group</param>
|
||||
/// <param name="view">The texture to add as an overlap</param>
|
||||
public void RemoveOverlap(int offset, Texture view)
|
||||
{
|
||||
// Overlaps can be accessed from the memory tracking signal handler, so access must be atomic.
|
||||
|
||||
if (OverlapsWith(offset, (int)view.Size))
|
||||
{
|
||||
lock (Overlaps)
|
||||
{
|
||||
Overlaps.Remove(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a sync action to happen for this handle, and an interim flush action on the tracking handle.
|
||||
/// </summary>
|
||||
|
@@ -1,9 +1,12 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.Gpu.Memory;
|
||||
using Ryujinx.Graphics.Texture;
|
||||
using Ryujinx.Memory.Range;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
@@ -12,8 +15,63 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
class TexturePool : Pool<Texture, TextureDescriptor>, IPool<TexturePool>
|
||||
{
|
||||
/// <summary>
|
||||
/// A request to dereference a texture from a pool.
|
||||
/// </summary>
|
||||
private struct DereferenceRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the dereference is due to a mapping change or not.
|
||||
/// </summary>
|
||||
public readonly bool IsRemapped;
|
||||
|
||||
/// <summary>
|
||||
/// The texture being dereferenced.
|
||||
/// </summary>
|
||||
public readonly Texture Texture;
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the pool entry this reference belonged to.
|
||||
/// </summary>
|
||||
public readonly int ID;
|
||||
|
||||
/// <summary>
|
||||
/// Create a dereference request for a texture with a specific pool ID, and remapped flag.
|
||||
/// </summary>
|
||||
/// <param name="isRemapped">Whether the dereference is due to a mapping change or not</param>
|
||||
/// <param name="texture">The texture being dereferenced</param>
|
||||
/// <param name="id">The ID of the pool entry, used to restore remapped textures</param>
|
||||
private DereferenceRequest(bool isRemapped, Texture texture, int id)
|
||||
{
|
||||
IsRemapped = isRemapped;
|
||||
Texture = texture;
|
||||
ID = id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a dereference request for a texture removal.
|
||||
/// </summary>
|
||||
/// <param name="texture">The texture being removed</param>
|
||||
/// <returns>A texture removal dereference request</returns>
|
||||
public static DereferenceRequest Remove(Texture texture)
|
||||
{
|
||||
return new DereferenceRequest(false, texture, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a dereference request for a texture remapping with a specific pool ID.
|
||||
/// </summary>
|
||||
/// <param name="texture">The texture being remapped</param>
|
||||
/// <param name="id">The ID of the pool entry, used to restore remapped textures</param>
|
||||
/// <returns>A remap dereference request</returns>
|
||||
public static DereferenceRequest Remap(Texture texture, int id)
|
||||
{
|
||||
return new DereferenceRequest(true, texture, id);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly GpuChannel _channel;
|
||||
private readonly ConcurrentQueue<Texture> _dereferenceQueue = new ConcurrentQueue<Texture>();
|
||||
private readonly ConcurrentQueue<DereferenceRequest> _dereferenceQueue = new ConcurrentQueue<DereferenceRequest>();
|
||||
private TextureDescriptor _defaultDescriptor;
|
||||
|
||||
/// <summary>
|
||||
@@ -58,7 +116,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
TextureInfo info = GetInfo(descriptor, out int layerSize);
|
||||
|
||||
ProcessDereferenceQueue();
|
||||
// The dereference queue can put our texture back on the cache.
|
||||
if ((texture = ProcessDereferenceQueue(id)) != null)
|
||||
{
|
||||
return ref descriptor;
|
||||
}
|
||||
|
||||
texture = PhysicalMemory.TextureCache.FindOrCreateTexture(_channel.MemoryManager, TextureSearchFlags.ForSampler, info, layerSize);
|
||||
|
||||
@@ -69,10 +131,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
}
|
||||
}
|
||||
|
||||
texture.IncrementReferenceCount(this, id);
|
||||
|
||||
Items[id] = texture;
|
||||
|
||||
texture.IncrementReferenceCount(this, id, descriptor.UnpackAddress());
|
||||
|
||||
DescriptorCache[id] = descriptor;
|
||||
}
|
||||
else
|
||||
@@ -155,11 +217,14 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <param name="deferred">If true, queue the dereference to happen on the render thread, otherwise dereference immediately</param>
|
||||
public void ForceRemove(Texture texture, int id, bool deferred)
|
||||
{
|
||||
Items[id] = null;
|
||||
var previous = Interlocked.Exchange(ref Items[id], null);
|
||||
|
||||
if (deferred)
|
||||
{
|
||||
_dereferenceQueue.Enqueue(texture);
|
||||
if (previous != null)
|
||||
{
|
||||
_dereferenceQueue.Enqueue(DereferenceRequest.Remove(texture));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -167,16 +232,91 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queues a request to update a texture's mapping.
|
||||
/// Mapping is updated later to avoid deleting the texture if it is still sparsely mapped.
|
||||
/// </summary>
|
||||
/// <param name="texture">Texture with potential mapping change</param>
|
||||
/// <param name="id">ID in cache of texture with potential mapping change</param>
|
||||
public void QueueUpdateMapping(Texture texture, int id)
|
||||
{
|
||||
if (Interlocked.Exchange(ref Items[id], null) == texture)
|
||||
{
|
||||
_dereferenceQueue.Enqueue(DereferenceRequest.Remap(texture, id));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the dereference queue, decrementing the reference count for each texture in it.
|
||||
/// This is used to ensure that texture disposal happens on the render thread.
|
||||
/// </summary>
|
||||
private void ProcessDereferenceQueue()
|
||||
/// <param name="id">The ID of the entry that triggered this method</param>
|
||||
/// <returns>Texture that matches the entry ID if it has been readded to the cache.</returns>
|
||||
private Texture ProcessDereferenceQueue(int id = -1)
|
||||
{
|
||||
while (_dereferenceQueue.TryDequeue(out Texture toRemove))
|
||||
while (_dereferenceQueue.TryDequeue(out DereferenceRequest request))
|
||||
{
|
||||
toRemove.DecrementReferenceCount();
|
||||
Texture texture = request.Texture;
|
||||
|
||||
// Unmapped storage textures can swap their ranges. The texture must be storage with no views or dependencies.
|
||||
// TODO: Would need to update ranges on views, or guarantee that ones where the range changes can be instantly deleted.
|
||||
|
||||
if (request.IsRemapped && texture.Group.Storage == texture && !texture.HasViews && !texture.Group.HasCopyDependencies)
|
||||
{
|
||||
// Has the mapping for this texture changed?
|
||||
ref readonly TextureDescriptor descriptor = ref GetDescriptorRef(request.ID);
|
||||
|
||||
ulong address = descriptor.UnpackAddress();
|
||||
|
||||
MultiRange range = _channel.MemoryManager.GetPhysicalRegions(address, texture.Size);
|
||||
|
||||
// If the texture is not mapped at all, delete its reference.
|
||||
|
||||
if (range.Count == 1 && range.GetSubRange(0).Address == MemoryManager.PteUnmapped)
|
||||
{
|
||||
texture.DecrementReferenceCount();
|
||||
continue;
|
||||
}
|
||||
|
||||
Items[request.ID] = texture;
|
||||
|
||||
// Create a new pool reference, as the last one was removed on unmap.
|
||||
|
||||
texture.IncrementReferenceCount(this, request.ID, address);
|
||||
texture.DecrementReferenceCount();
|
||||
|
||||
// Refetch the range. Changes since the last check could have been lost
|
||||
// as the cache entry was not restored (required to queue mapping change).
|
||||
|
||||
range = _channel.MemoryManager.GetPhysicalRegions(address, texture.Size);
|
||||
|
||||
if (!range.Equals(texture.Range))
|
||||
{
|
||||
// Part of the texture was mapped or unmapped. Replace the range and regenerate tracking handles.
|
||||
if (!_channel.MemoryManager.Physical.TextureCache.UpdateMapping(texture, range))
|
||||
{
|
||||
// Texture could not be remapped due to a collision, just delete it.
|
||||
if (Interlocked.Exchange(ref Items[request.ID], null) != null)
|
||||
{
|
||||
// If this is null, a request was already queued to decrement reference.
|
||||
texture.DecrementReferenceCount(this, request.ID);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (request.ID == id)
|
||||
{
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
texture.DecrementReferenceCount();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -213,9 +353,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
_channel.MemoryManager.Physical.TextureCache.AddShortCache(texture, ref cachedDescriptor);
|
||||
}
|
||||
|
||||
if (Interlocked.Exchange(ref Items[id], null) != null)
|
||||
{
|
||||
texture.DecrementReferenceCount(this, id);
|
||||
|
||||
Items[id] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -82,7 +82,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
Address = address;
|
||||
Size = size;
|
||||
|
||||
Handle = context.Renderer.CreateBuffer((int)size);
|
||||
Handle = context.Renderer.CreateBuffer((int)size, baseBuffers?.MaxBy(x => x.Size).Handle ?? BufferHandle.Null);
|
||||
|
||||
_useGranular = size > GranularBufferThreshold;
|
||||
|
||||
@@ -415,10 +415,10 @@ namespace Ryujinx.Graphics.Gpu.Memory
|
||||
{
|
||||
int offset = (int)(address - Address);
|
||||
|
||||
ReadOnlySpan<byte> data = _context.Renderer.GetBufferData(Handle, offset, (int)size);
|
||||
using PinnedSpan<byte> data = _context.Renderer.GetBufferData(Handle, offset, (int)size);
|
||||
|
||||
// TODO: When write tracking shaders, they will need to be aware of changes in overlapping buffers.
|
||||
_physicalMemory.WriteUntracked(address, data);
|
||||
_physicalMemory.WriteUntracked(address, data.Get());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@@ -1,3 +1,5 @@
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.Shader;
|
||||
using Ryujinx.Graphics.Shader.Translation;
|
||||
@@ -11,16 +13,15 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
|
||||
{
|
||||
public static byte[] Pack(ShaderSource[] sources)
|
||||
{
|
||||
using MemoryStream output = new MemoryStream();
|
||||
using BinaryWriter writer = new BinaryWriter(output);
|
||||
using MemoryStream output = MemoryStreamManager.Shared.GetStream();
|
||||
|
||||
writer.Write(sources.Length);
|
||||
output.Write(sources.Length);
|
||||
|
||||
for (int i = 0; i < sources.Length; i++)
|
||||
foreach (ShaderSource source in sources)
|
||||
{
|
||||
writer.Write((int)sources[i].Stage);
|
||||
writer.Write(sources[i].BinaryCode.Length);
|
||||
writer.Write(sources[i].BinaryCode);
|
||||
output.Write((int)source.Stage);
|
||||
output.Write(source.BinaryCode.Length);
|
||||
output.Write(source.BinaryCode);
|
||||
}
|
||||
|
||||
return output.ToArray();
|
||||
|
@@ -55,11 +55,14 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
(IntPtr)size);
|
||||
}
|
||||
|
||||
public static unsafe ReadOnlySpan<byte> GetData(OpenGLRenderer renderer, BufferHandle buffer, int offset, int size)
|
||||
public static unsafe PinnedSpan<byte> GetData(OpenGLRenderer renderer, BufferHandle buffer, int offset, int size)
|
||||
{
|
||||
// Data in the persistent buffer and host array is guaranteed to be available
|
||||
// until the next time the host thread requests data.
|
||||
|
||||
if (HwCapabilities.UsePersistentBufferForFlush)
|
||||
{
|
||||
return renderer.PersistentBuffers.Default.GetBufferData(buffer, offset, size);
|
||||
return PinnedSpan<byte>.UnsafeFromSpan(renderer.PersistentBuffers.Default.GetBufferData(buffer, offset, size));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -69,7 +72,7 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
|
||||
GL.GetBufferSubData(BufferTarget.CopyReadBuffer, (IntPtr)offset, size, target);
|
||||
|
||||
return new ReadOnlySpan<byte>(target.ToPointer(), size);
|
||||
return new PinnedSpan<byte>(target.ToPointer(), size);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -39,12 +39,12 @@ namespace Ryujinx.Graphics.OpenGL.Image
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public ReadOnlySpan<byte> GetData()
|
||||
public PinnedSpan<byte> GetData()
|
||||
{
|
||||
return Buffer.GetData(_renderer, _buffer, _bufferOffset, _bufferSize);
|
||||
}
|
||||
|
||||
public ReadOnlySpan<byte> GetData(int layer, int level)
|
||||
public PinnedSpan<byte> GetData(int layer, int level)
|
||||
{
|
||||
return GetData();
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user