mirror of
https://github.com/ryujinx-mirror/ryujinx.git
synced 2024-11-23 02:25:12 +00:00
Add an ASTC Decoder (Not currently used in Ryujinx) (#131)
* Add an ASTC Decoder (Not currently used in Ryujinx) * Update ASTCDecoder.cs
This commit is contained in:
parent
f43dd08064
commit
aeb1bbf50c
4 changed files with 1911 additions and 0 deletions
1384
Ryujinx.Graphics/Gal/Texture/ASTCDecoder.cs
Normal file
1384
Ryujinx.Graphics/Gal/Texture/ASTCDecoder.cs
Normal file
File diff suppressed because it is too large
Load diff
138
Ryujinx.Graphics/Gal/Texture/ASTCPixel.cs
Normal file
138
Ryujinx.Graphics/Gal/Texture/ASTCPixel.cs
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Ryujinx.Graphics.Gal.Texture
|
||||||
|
{
|
||||||
|
class ASTCPixel
|
||||||
|
{
|
||||||
|
public short R { get; set; }
|
||||||
|
public short G { get; set; }
|
||||||
|
public short B { get; set; }
|
||||||
|
public short A { get; set; }
|
||||||
|
|
||||||
|
byte[] BitDepth = new byte[4];
|
||||||
|
|
||||||
|
public ASTCPixel(short _A, short _R, short _G, short _B)
|
||||||
|
{
|
||||||
|
A = _A;
|
||||||
|
R = _R;
|
||||||
|
G = _G;
|
||||||
|
B = _B;
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
BitDepth[i] = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClampByte()
|
||||||
|
{
|
||||||
|
R = Math.Min(Math.Max(R, (short)0), (short)255);
|
||||||
|
G = Math.Min(Math.Max(G, (short)0), (short)255);
|
||||||
|
B = Math.Min(Math.Max(B, (short)0), (short)255);
|
||||||
|
A = Math.Min(Math.Max(A, (short)0), (short)255);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short GetComponent(int Index)
|
||||||
|
{
|
||||||
|
switch(Index)
|
||||||
|
{
|
||||||
|
case 0: return A;
|
||||||
|
case 1: return R;
|
||||||
|
case 2: return G;
|
||||||
|
case 3: return B;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetComponent(int Index, int Value)
|
||||||
|
{
|
||||||
|
switch (Index)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
A = (short)Value;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
R = (short)Value;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
G = (short)Value;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
B = (short)Value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeBitDepth(byte[] Depth)
|
||||||
|
{
|
||||||
|
for(int i = 0; i< 4; i++)
|
||||||
|
{
|
||||||
|
int Value = ChangeBitDepth(GetComponent(i), BitDepth[i], Depth[i]);
|
||||||
|
|
||||||
|
SetComponent(i, Value);
|
||||||
|
BitDepth[i] = Depth[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
short ChangeBitDepth(short Value, byte OldDepth, byte NewDepth)
|
||||||
|
{
|
||||||
|
Debug.Assert(NewDepth <= 8);
|
||||||
|
Debug.Assert(OldDepth <= 8);
|
||||||
|
|
||||||
|
if (OldDepth == NewDepth)
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
return Value;
|
||||||
|
}
|
||||||
|
else if (OldDepth == 0 && NewDepth != 0)
|
||||||
|
{
|
||||||
|
return (short)((1 << NewDepth) - 1);
|
||||||
|
}
|
||||||
|
else if (NewDepth > OldDepth)
|
||||||
|
{
|
||||||
|
return (short)BitArrayStream.Replicate(Value, OldDepth, NewDepth);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// oldDepth > newDepth
|
||||||
|
if (NewDepth == 0)
|
||||||
|
{
|
||||||
|
return 0xFF;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
byte BitsWasted = (byte)(OldDepth - NewDepth);
|
||||||
|
short TempValue = Value;
|
||||||
|
|
||||||
|
TempValue = (short)((TempValue + (1 << (BitsWasted - 1))) >> BitsWasted);
|
||||||
|
TempValue = Math.Min(Math.Max((short)0, TempValue), (short)((1 << NewDepth) - 1));
|
||||||
|
|
||||||
|
return (byte)(TempValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Pack()
|
||||||
|
{
|
||||||
|
ASTCPixel NewPixel = new ASTCPixel(A, R, G, B);
|
||||||
|
byte[] eightBitDepth = { 8, 8, 8, 8 };
|
||||||
|
|
||||||
|
NewPixel.ChangeBitDepth(eightBitDepth);
|
||||||
|
|
||||||
|
return (byte)NewPixel.A << 24 |
|
||||||
|
(byte)NewPixel.B << 16 |
|
||||||
|
(byte)NewPixel.G << 8 |
|
||||||
|
(byte)NewPixel.R << 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds more precision to the blue channel as described
|
||||||
|
// in C.2.14
|
||||||
|
public static ASTCPixel BlueContract(int a, int r, int g, int b)
|
||||||
|
{
|
||||||
|
return new ASTCPixel((short)(a),
|
||||||
|
(short)((r + b) >> 1),
|
||||||
|
(short)((g + b) >> 1),
|
||||||
|
(short)(b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
120
Ryujinx.Graphics/Gal/Texture/BitArrayStream.cs
Normal file
120
Ryujinx.Graphics/Gal/Texture/BitArrayStream.cs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace Ryujinx.Graphics.Gal.Texture
|
||||||
|
{
|
||||||
|
public class BitArrayStream
|
||||||
|
{
|
||||||
|
public BitArray BitsArray;
|
||||||
|
public int Position { get; private set; }
|
||||||
|
|
||||||
|
public BitArrayStream(BitArray BitArray)
|
||||||
|
{
|
||||||
|
BitsArray = BitArray;
|
||||||
|
Position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short ReadBits(int Length)
|
||||||
|
{
|
||||||
|
int RetValue = 0;
|
||||||
|
for (int i = Position; i < Position + Length; i++)
|
||||||
|
{
|
||||||
|
if (BitsArray[i])
|
||||||
|
{
|
||||||
|
RetValue |= 1 << (i - Position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Position += Length;
|
||||||
|
return (short)RetValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ReadBits(int Start, int End)
|
||||||
|
{
|
||||||
|
int RetValue = 0;
|
||||||
|
for (int i = Start; i <= End; i++)
|
||||||
|
{
|
||||||
|
if (BitsArray[i])
|
||||||
|
{
|
||||||
|
RetValue |= 1 << (i - Start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RetValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ReadBit(int Index)
|
||||||
|
{
|
||||||
|
return Convert.ToInt32(BitsArray[Index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteBits(int Value, int Length)
|
||||||
|
{
|
||||||
|
for (int i = Position; i < Position + Length; i++)
|
||||||
|
{
|
||||||
|
BitsArray[i] = ((Value >> (i - Position)) & 1) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Position += Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] ToByteArray()
|
||||||
|
{
|
||||||
|
byte[] RetArray = new byte[(BitsArray.Length + 7) / 8];
|
||||||
|
BitsArray.CopyTo(RetArray, 0);
|
||||||
|
return RetArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int Replicate(int Value, int NumberBits, int ToBit)
|
||||||
|
{
|
||||||
|
if (NumberBits == 0) return 0;
|
||||||
|
if (ToBit == 0) return 0;
|
||||||
|
|
||||||
|
int TempValue = Value & ((1 << NumberBits) - 1);
|
||||||
|
int RetValue = TempValue;
|
||||||
|
int ResLength = NumberBits;
|
||||||
|
|
||||||
|
while (ResLength < ToBit)
|
||||||
|
{
|
||||||
|
int Comp = 0;
|
||||||
|
if (NumberBits > ToBit - ResLength)
|
||||||
|
{
|
||||||
|
int NewShift = ToBit - ResLength;
|
||||||
|
Comp = NumberBits - NewShift;
|
||||||
|
NumberBits = NewShift;
|
||||||
|
}
|
||||||
|
RetValue <<= NumberBits;
|
||||||
|
RetValue |= TempValue >> Comp;
|
||||||
|
ResLength += NumberBits;
|
||||||
|
}
|
||||||
|
return RetValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int PopCnt(int Number)
|
||||||
|
{
|
||||||
|
int Counter;
|
||||||
|
for (Counter = 0; Number != 0; Counter++)
|
||||||
|
{
|
||||||
|
Number &= Number - 1;
|
||||||
|
}
|
||||||
|
return Counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Swap<T>(ref T lhs, ref T rhs)
|
||||||
|
{
|
||||||
|
T Temp = lhs;
|
||||||
|
lhs = rhs;
|
||||||
|
rhs = Temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfers a bit as described in C.2.14
|
||||||
|
public static void BitTransferSigned(ref int a, ref int b)
|
||||||
|
{
|
||||||
|
b >>= 1;
|
||||||
|
b |= a & 0x80;
|
||||||
|
a >>= 1;
|
||||||
|
a &= 0x3F;
|
||||||
|
if ((a & 0x20) != 0) a -= 0x40;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
269
Ryujinx.Graphics/Gal/Texture/IntegerEncoded.cs
Normal file
269
Ryujinx.Graphics/Gal/Texture/IntegerEncoded.cs
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ryujinx.Graphics.Gal.Texture
|
||||||
|
{
|
||||||
|
public struct IntegerEncoded
|
||||||
|
{
|
||||||
|
public enum EIntegerEncoding
|
||||||
|
{
|
||||||
|
JustBits,
|
||||||
|
Quint,
|
||||||
|
Trit
|
||||||
|
}
|
||||||
|
|
||||||
|
EIntegerEncoding Encoding;
|
||||||
|
public int NumberBits { get; private set; }
|
||||||
|
public int BitValue { get; private set; }
|
||||||
|
public int TritValue { get; private set; }
|
||||||
|
public int QuintValue { get; private set; }
|
||||||
|
|
||||||
|
public IntegerEncoded(EIntegerEncoding _Encoding, int NumBits)
|
||||||
|
{
|
||||||
|
Encoding = _Encoding;
|
||||||
|
NumberBits = NumBits;
|
||||||
|
BitValue = 0;
|
||||||
|
TritValue = 0;
|
||||||
|
QuintValue = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MatchesEncoding(IntegerEncoded Other)
|
||||||
|
{
|
||||||
|
return Encoding == Other.Encoding && NumberBits == Other.NumberBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EIntegerEncoding GetEncoding()
|
||||||
|
{
|
||||||
|
return Encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetBitLength(int NumberVals)
|
||||||
|
{
|
||||||
|
int TotalBits = NumberBits * NumberVals;
|
||||||
|
if (Encoding == EIntegerEncoding.Trit)
|
||||||
|
{
|
||||||
|
TotalBits += (NumberVals * 8 + 4) / 5;
|
||||||
|
}
|
||||||
|
else if (Encoding == EIntegerEncoding.Quint)
|
||||||
|
{
|
||||||
|
TotalBits += (NumberVals * 7 + 2) / 3;
|
||||||
|
}
|
||||||
|
return TotalBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IntegerEncoded CreateEncoding(int MaxVal)
|
||||||
|
{
|
||||||
|
while (MaxVal > 0)
|
||||||
|
{
|
||||||
|
int Check = MaxVal + 1;
|
||||||
|
|
||||||
|
// Is maxVal a power of two?
|
||||||
|
if ((Check & (Check - 1)) == 0)
|
||||||
|
{
|
||||||
|
return new IntegerEncoded(EIntegerEncoding.JustBits, BitArrayStream.PopCnt(MaxVal));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is maxVal of the type 3*2^n - 1?
|
||||||
|
if ((Check % 3 == 0) && ((Check / 3) & ((Check / 3) - 1)) == 0)
|
||||||
|
{
|
||||||
|
return new IntegerEncoded(EIntegerEncoding.Trit, BitArrayStream.PopCnt(Check / 3 - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is maxVal of the type 5*2^n - 1?
|
||||||
|
if ((Check % 5 == 0) && ((Check / 5) & ((Check / 5) - 1)) == 0)
|
||||||
|
{
|
||||||
|
return new IntegerEncoded(EIntegerEncoding.Quint, BitArrayStream.PopCnt(Check / 5 - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apparently it can't be represented with a bounded integer sequence...
|
||||||
|
// just iterate.
|
||||||
|
MaxVal--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new IntegerEncoded(EIntegerEncoding.JustBits, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DecodeTritBlock(
|
||||||
|
BitArrayStream BitStream,
|
||||||
|
List<IntegerEncoded> ListIntegerEncoded,
|
||||||
|
int NumberBitsPerValue)
|
||||||
|
{
|
||||||
|
// Implement the algorithm in section C.2.12
|
||||||
|
int[] m = new int[5];
|
||||||
|
int[] t = new int[5];
|
||||||
|
int T;
|
||||||
|
|
||||||
|
// Read the trit encoded block according to
|
||||||
|
// table C.2.14
|
||||||
|
m[0] = BitStream.ReadBits(NumberBitsPerValue);
|
||||||
|
T = BitStream.ReadBits(2);
|
||||||
|
m[1] = BitStream.ReadBits(NumberBitsPerValue);
|
||||||
|
T |= BitStream.ReadBits(2) << 2;
|
||||||
|
m[2] = BitStream.ReadBits(NumberBitsPerValue);
|
||||||
|
T |= BitStream.ReadBits(1) << 4;
|
||||||
|
m[3] = BitStream.ReadBits(NumberBitsPerValue);
|
||||||
|
T |= BitStream.ReadBits(2) << 5;
|
||||||
|
m[4] = BitStream.ReadBits(NumberBitsPerValue);
|
||||||
|
T |= BitStream.ReadBits(1) << 7;
|
||||||
|
|
||||||
|
int C = 0;
|
||||||
|
|
||||||
|
BitArrayStream Tb = new BitArrayStream(new BitArray(new int[] { T }));
|
||||||
|
if (Tb.ReadBits(2, 4) == 7)
|
||||||
|
{
|
||||||
|
C = (Tb.ReadBits(5, 7) << 2) | Tb.ReadBits(0, 1);
|
||||||
|
t[4] = t[3] = 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
C = Tb.ReadBits(0, 4);
|
||||||
|
if (Tb.ReadBits(5, 6) == 3)
|
||||||
|
{
|
||||||
|
t[4] = 2;
|
||||||
|
t[3] = Tb.ReadBit(7);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
t[4] = Tb.ReadBit(7);
|
||||||
|
t[3] = Tb.ReadBits(5, 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BitArrayStream Cb = new BitArrayStream(new BitArray(new int[] { C }));
|
||||||
|
if (Cb.ReadBits(0, 1) == 3)
|
||||||
|
{
|
||||||
|
t[2] = 2;
|
||||||
|
t[1] = Cb.ReadBit(4);
|
||||||
|
t[0] = (Cb.ReadBit(3) << 1) | (Cb.ReadBit(2) & ~Cb.ReadBit(3));
|
||||||
|
}
|
||||||
|
else if (Cb.ReadBits(2, 3) == 3)
|
||||||
|
{
|
||||||
|
t[2] = 2;
|
||||||
|
t[1] = 2;
|
||||||
|
t[0] = Cb.ReadBits(0, 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
t[2] = Cb.ReadBit(4);
|
||||||
|
t[1] = Cb.ReadBits(2, 3);
|
||||||
|
t[0] = (Cb.ReadBit(1) << 1) | (Cb.ReadBit(0) & ~Cb.ReadBit(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
IntegerEncoded IntEncoded = new IntegerEncoded(EIntegerEncoding.Trit, NumberBitsPerValue)
|
||||||
|
{
|
||||||
|
BitValue = m[i],
|
||||||
|
TritValue = t[i]
|
||||||
|
};
|
||||||
|
ListIntegerEncoded.Add(IntEncoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DecodeQuintBlock(
|
||||||
|
BitArrayStream BitStream,
|
||||||
|
List<IntegerEncoded> ListIntegerEncoded,
|
||||||
|
int NumberBitsPerValue)
|
||||||
|
{
|
||||||
|
// Implement the algorithm in section C.2.12
|
||||||
|
int[] m = new int[3];
|
||||||
|
int[] q = new int[3];
|
||||||
|
int Q;
|
||||||
|
|
||||||
|
// Read the trit encoded block according to
|
||||||
|
// table C.2.15
|
||||||
|
m[0] = BitStream.ReadBits(NumberBitsPerValue);
|
||||||
|
Q = BitStream.ReadBits(3);
|
||||||
|
m[1] = BitStream.ReadBits(NumberBitsPerValue);
|
||||||
|
Q |= BitStream.ReadBits(2) << 3;
|
||||||
|
m[2] = BitStream.ReadBits(NumberBitsPerValue);
|
||||||
|
Q |= BitStream.ReadBits(2) << 5;
|
||||||
|
|
||||||
|
BitArrayStream Qb = new BitArrayStream(new BitArray(new int[] { Q }));
|
||||||
|
if (Qb.ReadBits(1, 2) == 3 && Qb.ReadBits(5, 6) == 0)
|
||||||
|
{
|
||||||
|
q[0] = q[1] = 4;
|
||||||
|
q[2] = (Qb.ReadBit(0) << 2) | ((Qb.ReadBit(4) & ~Qb.ReadBit(0)) << 1) | (Qb.ReadBit(3) & ~Qb.ReadBit(0));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int C = 0;
|
||||||
|
if (Qb.ReadBits(1, 2) == 3)
|
||||||
|
{
|
||||||
|
q[2] = 4;
|
||||||
|
C = (Qb.ReadBits(3, 4) << 3) | ((~Qb.ReadBits(5, 6) & 3) << 1) | Qb.ReadBit(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
q[2] = Qb.ReadBits(5, 6);
|
||||||
|
C = Qb.ReadBits(0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
BitArrayStream Cb = new BitArrayStream(new BitArray(new int[] { C }));
|
||||||
|
if (Cb.ReadBits(0, 2) == 5)
|
||||||
|
{
|
||||||
|
q[1] = 4;
|
||||||
|
q[0] = Cb.ReadBits(3, 4);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
q[1] = Cb.ReadBits(3, 4);
|
||||||
|
q[0] = Cb.ReadBits(0, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
IntegerEncoded IntEncoded = new IntegerEncoded(EIntegerEncoding.Quint, NumberBitsPerValue)
|
||||||
|
{
|
||||||
|
BitValue = m[i],
|
||||||
|
QuintValue = q[i]
|
||||||
|
};
|
||||||
|
ListIntegerEncoded.Add(IntEncoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DecodeIntegerSequence(
|
||||||
|
List<IntegerEncoded> DecodeIntegerSequence,
|
||||||
|
BitArrayStream BitStream,
|
||||||
|
int MaxRange,
|
||||||
|
int NumberValues)
|
||||||
|
{
|
||||||
|
// Determine encoding parameters
|
||||||
|
IntegerEncoded IntEncoded = CreateEncoding(MaxRange);
|
||||||
|
|
||||||
|
// Start decoding
|
||||||
|
int NumberValuesDecoded = 0;
|
||||||
|
while (NumberValuesDecoded < NumberValues)
|
||||||
|
{
|
||||||
|
switch (IntEncoded.GetEncoding())
|
||||||
|
{
|
||||||
|
case EIntegerEncoding.Quint:
|
||||||
|
{
|
||||||
|
DecodeQuintBlock(BitStream, DecodeIntegerSequence, IntEncoded.NumberBits);
|
||||||
|
NumberValuesDecoded += 3;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EIntegerEncoding.Trit:
|
||||||
|
{
|
||||||
|
DecodeTritBlock(BitStream, DecodeIntegerSequence, IntEncoded.NumberBits);
|
||||||
|
NumberValuesDecoded += 5;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EIntegerEncoding.JustBits:
|
||||||
|
{
|
||||||
|
IntEncoded.BitValue = BitStream.ReadBits(IntEncoded.NumberBits);
|
||||||
|
DecodeIntegerSequence.Add(IntEncoded);
|
||||||
|
NumberValuesDecoded++;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue