0
0
Fork 0
mirror of https://github.com/ryujinx-mirror/ryujinx.git synced 2024-12-26 07:25:45 +00:00
ryujinx-fork/Ryujinx.Graphics/Gal/Shader/ShaderDecodeMem.cs
Alex Barney 1f554c1093 Do naming refactoring on Ryujinx.Graphics (#611)
* Renaming part 1

* Renaming part 2

* Renaming part 3

* Renaming part 4

* Renaming part 5

* Renaming part 6

* Renaming part 7

* Renaming part 8

* Renaming part 9

* Renaming part 10

* General cleanup

* Thought I got all of these

* Apply #595

* Additional renaming

* Tweaks from feedback

* Rename files
2019-03-04 12:45:25 +11:00

878 lines
No EOL
29 KiB
C#

using Ryujinx.Graphics.Texture;
using System;
using static Ryujinx.Graphics.Gal.Shader.ShaderDecodeHelper;
namespace Ryujinx.Graphics.Gal.Shader
{
static partial class ShaderDecode
{
// ReSharper disable InconsistentNaming
private const int ____ = 0x0;
private const int R___ = 0x1;
private const int _G__ = 0x2;
private const int RG__ = 0x3;
private const int __B_ = 0x4;
private const int RGB_ = 0x7;
private const int ___A = 0x8;
private const int R__A = 0x9;
private const int _G_A = 0xa;
private const int RG_A = 0xb;
private const int __BA = 0xc;
private const int R_BA = 0xd;
private const int _GBA = 0xe;
private const int RGBA = 0xf;
// ReSharper restore InconsistentNaming
private static int[,] _maskLut = new int[,]
{
{ ____, ____, ____, ____, ____, ____, ____, ____ },
{ R___, _G__, __B_, ___A, RG__, R__A, _G_A, __BA },
{ R___, _G__, __B_, ___A, RG__, ____, ____, ____ },
{ RGB_, RG_A, R_BA, _GBA, RGBA, ____, ____, ____ }
};
private static GalTextureTarget TexToTextureTarget(int texType, bool isArray)
{
switch (texType)
{
case 0:
return isArray ? GalTextureTarget.OneDArray : GalTextureTarget.OneD;
case 2:
return isArray ? GalTextureTarget.TwoDArray : GalTextureTarget.TwoD;
case 4:
if (isArray)
throw new InvalidOperationException("ARRAY bit set on a TEX with 3D texture!");
return GalTextureTarget.ThreeD;
case 6:
return isArray ? GalTextureTarget.CubeArray : GalTextureTarget.CubeMap;
default:
throw new InvalidOperationException();
}
}
private static GalTextureTarget TexsToTextureTarget(int texType)
{
switch (texType)
{
case 0:
return GalTextureTarget.OneD;
case 2:
case 4:
case 6:
case 8:
case 0xa:
case 0xc:
return GalTextureTarget.TwoD;
case 0xe:
case 0x10:
case 0x12:
return GalTextureTarget.TwoDArray;
case 0x14:
case 0x16:
return GalTextureTarget.ThreeD;
case 0x18:
case 0x1a:
return GalTextureTarget.CubeMap;
default:
throw new InvalidOperationException();
}
}
public static GalTextureTarget TldsToTextureTarget(int texType)
{
switch (texType)
{
case 0:
case 2:
return GalTextureTarget.OneD;
case 4:
case 8:
case 0xa:
case 0xc:
case 0x18:
return GalTextureTarget.TwoD;
case 0x10:
return GalTextureTarget.TwoDArray;
case 0xe:
return GalTextureTarget.ThreeD;
default:
throw new InvalidOperationException();
}
}
public static void Ld_A(ShaderIrBlock block, long opCode, int position)
{
ShaderIrNode[] opers = opCode.Abuf20();
//Used by GS
ShaderIrOperGpr vertex = opCode.Gpr39();
int index = 0;
foreach (ShaderIrNode operA in opers)
{
ShaderIrOperGpr operD = opCode.Gpr0();
operD.Index += index++;
block.AddNode(opCode.PredNode(new ShaderIrAsg(operD, operA)));
}
}
public static void Ld_C(ShaderIrBlock block, long opCode, int position)
{
int cbufPos = opCode.Read(22, 0x3fff);
int cbufIndex = opCode.Read(36, 0x1f);
int type = opCode.Read(48, 7);
if (type > 5)
{
throw new InvalidOperationException();
}
ShaderIrOperGpr temp = ShaderIrOperGpr.MakeTemporary();
block.AddNode(new ShaderIrAsg(temp, opCode.Gpr8()));
int count = type == 5 ? 2 : 1;
for (int index = 0; index < count; index++)
{
ShaderIrOperCbuf operA = new ShaderIrOperCbuf(cbufIndex, cbufPos, temp);
ShaderIrOperGpr operD = opCode.Gpr0();
operA.Pos += index;
operD.Index += index;
if (!operD.IsValidRegister)
{
break;
}
ShaderIrNode node = operA;
if (type < 4)
{
//This is a 8 or 16 bits type.
bool signed = (type & 1) != 0;
int size = 8 << (type >> 1);
node = ExtendTo32(node, signed, size);
}
block.AddNode(opCode.PredNode(new ShaderIrAsg(operD, node)));
}
}
public static void St_A(ShaderIrBlock block, long opCode, int position)
{
ShaderIrNode[] opers = opCode.Abuf20();
int index = 0;
foreach (ShaderIrNode operA in opers)
{
ShaderIrOperGpr operD = opCode.Gpr0();
operD.Index += index++;
block.AddNode(opCode.PredNode(new ShaderIrAsg(operA, operD)));
}
}
public static void Texq(ShaderIrBlock block, long opCode, int position)
{
ShaderIrNode operD = opCode.Gpr0();
ShaderIrNode operA = opCode.Gpr8();
ShaderTexqInfo info = (ShaderTexqInfo)(opCode.Read(22, 0x1f));
ShaderIrMetaTexq meta0 = new ShaderIrMetaTexq(info, 0);
ShaderIrMetaTexq meta1 = new ShaderIrMetaTexq(info, 1);
ShaderIrNode operC = opCode.Imm13_36();
ShaderIrOp op0 = new ShaderIrOp(ShaderIrInst.Texq, operA, null, operC, meta0);
ShaderIrOp op1 = new ShaderIrOp(ShaderIrInst.Texq, operA, null, operC, meta1);
block.AddNode(opCode.PredNode(new ShaderIrAsg(operD, op0)));
block.AddNode(opCode.PredNode(new ShaderIrAsg(operA, op1))); //Is this right?
}
public static void Tex(ShaderIrBlock block, long opCode, int position)
{
TextureInstructionSuffix suffix;
int rawSuffix = opCode.Read(0x34, 0x38);
switch (rawSuffix)
{
case 0:
suffix = TextureInstructionSuffix.None;
break;
case 0x8:
suffix = TextureInstructionSuffix.Lz;
break;
case 0x10:
suffix = TextureInstructionSuffix.Lb;
break;
case 0x18:
suffix = TextureInstructionSuffix.Ll;
break;
case 0x30:
suffix = TextureInstructionSuffix.Lba;
break;
case 0x38:
suffix = TextureInstructionSuffix.Lla;
break;
default:
throw new InvalidOperationException($"Invalid Suffix for TEX instruction {rawSuffix}");
}
bool isOffset = opCode.Read(0x36);
if (isOffset)
suffix |= TextureInstructionSuffix.AOffI;
EmitTex(block, opCode, suffix, gprHandle: false);
}
public static void Tex_B(ShaderIrBlock block, long opCode, int position)
{
TextureInstructionSuffix suffix;
int rawSuffix = opCode.Read(0x24, 0xe);
switch (rawSuffix)
{
case 0:
suffix = TextureInstructionSuffix.None;
break;
case 0x2:
suffix = TextureInstructionSuffix.Lz;
break;
case 0x4:
suffix = TextureInstructionSuffix.Lb;
break;
case 0x6:
suffix = TextureInstructionSuffix.Ll;
break;
case 0xc:
suffix = TextureInstructionSuffix.Lba;
break;
case 0xe:
suffix = TextureInstructionSuffix.Lla;
break;
default:
throw new InvalidOperationException($"Invalid Suffix for TEX.B instruction {rawSuffix}");
}
bool isOffset = opCode.Read(0x23);
if (isOffset)
suffix |= TextureInstructionSuffix.AOffI;
EmitTex(block, opCode, suffix, gprHandle: true);
}
private static void EmitTex(ShaderIrBlock block, long opCode, TextureInstructionSuffix textureInstructionSuffix, bool gprHandle)
{
bool isArray = opCode.HasArray();
GalTextureTarget textureTarget = TexToTextureTarget(opCode.Read(28, 6), isArray);
bool hasDepthCompare = opCode.Read(0x32);
if (hasDepthCompare)
{
textureInstructionSuffix |= TextureInstructionSuffix.Dc;
}
ShaderIrOperGpr[] coords = new ShaderIrOperGpr[ImageUtils.GetCoordsCountTextureTarget(textureTarget)];
int indexExtraCoord = 0;
if (isArray)
{
indexExtraCoord++;
coords[coords.Length - 1] = opCode.Gpr8();
}
for (int index = 0; index < coords.Length - indexExtraCoord; index++)
{
ShaderIrOperGpr coordReg = opCode.Gpr8();
coordReg.Index += index;
coordReg.Index += indexExtraCoord;
if (!coordReg.IsValidRegister)
{
coordReg.Index = ShaderIrOperGpr.ZrIndex;
}
coords[index] = coordReg;
}
int chMask = opCode.Read(31, 0xf);
ShaderIrOperGpr levelOfDetail = null;
ShaderIrOperGpr offset = null;
ShaderIrOperGpr depthCompare = null;
// TODO: determine first argument when TEX.B is used
int operBIndex = gprHandle ? 1 : 0;
if ((textureInstructionSuffix & TextureInstructionSuffix.Ll) != 0 ||
(textureInstructionSuffix & TextureInstructionSuffix.Lb) != 0 ||
(textureInstructionSuffix & TextureInstructionSuffix.Lba) != 0 ||
(textureInstructionSuffix & TextureInstructionSuffix.Lla) != 0)
{
levelOfDetail = opCode.Gpr20();
levelOfDetail.Index += operBIndex;
operBIndex++;
}
if ((textureInstructionSuffix & TextureInstructionSuffix.AOffI) != 0)
{
offset = opCode.Gpr20();
offset.Index += operBIndex;
operBIndex++;
}
if ((textureInstructionSuffix & TextureInstructionSuffix.Dc) != 0)
{
depthCompare = opCode.Gpr20();
depthCompare.Index += operBIndex;
operBIndex++;
}
// ???
ShaderIrNode operC = gprHandle
? (ShaderIrNode)opCode.Gpr20()
: (ShaderIrNode)opCode.Imm13_36();
ShaderIrInst inst = gprHandle ? ShaderIrInst.Texb : ShaderIrInst.Texs;
coords = CoordsRegistersToTempRegisters(block, coords);
int regInc = 0;
for (int ch = 0; ch < 4; ch++)
{
if (!IsChannelUsed(chMask, ch))
{
continue;
}
ShaderIrOperGpr dst = opCode.Gpr0();
dst.Index += regInc++;
if (!dst.IsValidRegister || dst.IsConst)
{
continue;
}
ShaderIrMetaTex meta = new ShaderIrMetaTex(ch, textureTarget, textureInstructionSuffix, coords)
{
LevelOfDetail = levelOfDetail,
Offset = offset,
DepthCompare = depthCompare
};
ShaderIrOp op = new ShaderIrOp(inst, coords[0], coords.Length > 1 ? coords[1] : null, operC, meta);
block.AddNode(opCode.PredNode(new ShaderIrAsg(dst, op)));
}
}
public static void Texs(ShaderIrBlock block, long opCode, int position)
{
TextureInstructionSuffix suffix;
int rawSuffix = opCode.Read(0x34, 0x1e);
switch (rawSuffix)
{
case 0:
case 0x4:
case 0x10:
case 0x16:
suffix = TextureInstructionSuffix.Lz;
break;
case 0x6:
case 0x1a:
suffix = TextureInstructionSuffix.Ll;
break;
case 0x8:
suffix = TextureInstructionSuffix.Dc;
break;
case 0x2:
case 0xe:
case 0x14:
case 0x18:
suffix = TextureInstructionSuffix.None;
break;
case 0xa:
suffix = TextureInstructionSuffix.Ll | TextureInstructionSuffix.Dc;
break;
case 0xc:
case 0x12:
suffix = TextureInstructionSuffix.Lz | TextureInstructionSuffix.Dc;
break;
default:
throw new InvalidOperationException($"Invalid Suffix for TEXS instruction {rawSuffix}");
}
GalTextureTarget textureTarget = TexsToTextureTarget(opCode.Read(52, 0x1e));
EmitTexs(block, opCode, ShaderIrInst.Texs, textureTarget, suffix);
}
public static void Tlds(ShaderIrBlock block, long opCode, int position)
{
TextureInstructionSuffix suffix;
int rawSuffix = opCode.Read(0x34, 0x1e);
switch (rawSuffix)
{
case 0:
case 0x4:
case 0x8:
suffix = TextureInstructionSuffix.Lz | TextureInstructionSuffix.AOffI;
break;
case 0xc:
suffix = TextureInstructionSuffix.Lz | TextureInstructionSuffix.Mz;
break;
case 0xe:
case 0x10:
suffix = TextureInstructionSuffix.Lz;
break;
case 0x2:
case 0xa:
suffix = TextureInstructionSuffix.Ll;
break;
case 0x18:
suffix = TextureInstructionSuffix.Ll | TextureInstructionSuffix.AOffI;
break;
default:
throw new InvalidOperationException($"Invalid Suffix for TLDS instruction {rawSuffix}");
}
GalTextureTarget textureTarget = TldsToTextureTarget(opCode.Read(52, 0x1e));
EmitTexs(block, opCode, ShaderIrInst.Txlf, textureTarget, suffix);
}
public static void Tld4(ShaderIrBlock block, long opCode, int position)
{
TextureInstructionSuffix suffix;
int rawSuffix = opCode.Read(0x34, 0xc);
switch (rawSuffix)
{
case 0:
suffix = TextureInstructionSuffix.None;
break;
case 0x4:
suffix = TextureInstructionSuffix.AOffI;
break;
case 0x8:
suffix = TextureInstructionSuffix.Ptp;
break;
default:
throw new InvalidOperationException($"Invalid Suffix for TLD4 instruction {rawSuffix}");
}
bool isShadow = opCode.Read(0x32);
bool isArray = opCode.HasArray();
int chMask = opCode.Read(31, 0xf);
GalTextureTarget textureTarget = TexToTextureTarget(opCode.Read(28, 6), isArray);
if (isShadow)
{
suffix |= TextureInstructionSuffix.Dc;
}
EmitTld4(block, opCode, textureTarget, suffix, chMask, opCode.Read(0x38, 0x3), false);
}
public static void Tld4S(ShaderIrBlock block, long opCode, int position)
{
TextureInstructionSuffix suffix = TextureInstructionSuffix.None;
bool isOffset = opCode.Read(0x33);
bool isShadow = opCode.Read(0x32);
if (isOffset)
{
suffix |= TextureInstructionSuffix.AOffI;
}
if (isShadow)
{
suffix |= TextureInstructionSuffix.Dc;
}
// TLD4S seems to only support 2D textures with RGBA mask?
EmitTld4(block, opCode, GalTextureTarget.TwoD, suffix, RGBA, opCode.Read(0x34, 0x3), true);
}
private static void EmitTexs(ShaderIrBlock block,
long opCode,
ShaderIrInst inst,
GalTextureTarget textureTarget,
TextureInstructionSuffix textureInstructionSuffix)
{
if (inst == ShaderIrInst.Txlf && textureTarget == GalTextureTarget.CubeArray)
{
throw new InvalidOperationException("TLDS instructions cannot use CUBE modifier!");
}
bool isArray = ImageUtils.IsArray(textureTarget);
ShaderIrOperGpr[] coords = new ShaderIrOperGpr[ImageUtils.GetCoordsCountTextureTarget(textureTarget)];
ShaderIrOperGpr operA = opCode.Gpr8();
ShaderIrOperGpr operB = opCode.Gpr20();
ShaderIrOperGpr suffixExtra = opCode.Gpr20();
suffixExtra.Index += 1;
int coordStartIndex = 0;
if (isArray)
{
coordStartIndex++;
coords[coords.Length - 1] = opCode.Gpr8();
}
switch (coords.Length - coordStartIndex)
{
case 1:
coords[0] = opCode.Gpr8();
break;
case 2:
coords[0] = opCode.Gpr8();
coords[0].Index += coordStartIndex;
break;
case 3:
coords[0] = opCode.Gpr8();
coords[0].Index += coordStartIndex;
coords[1] = opCode.Gpr8();
coords[1].Index += 1 + coordStartIndex;
break;
default:
throw new NotSupportedException($"{coords.Length - coordStartIndex} coords textures aren't supported in TEXS");
}
int operBIndex = 0;
ShaderIrOperGpr levelOfDetail = null;
ShaderIrOperGpr offset = null;
ShaderIrOperGpr depthCompare = null;
// OperB is always the last value
// Not applicable to 1d textures
if (coords.Length - coordStartIndex != 1)
{
coords[coords.Length - coordStartIndex - 1] = operB;
operBIndex++;
}
// Encoding of TEXS/TLDS is a bit special and change for 2d textures
// NOTE: OperA seems to hold at best two args.
// On 2D textures, if no suffix need an additional values, Y is stored in OperB, otherwise coords are in OperA and the additional values is in OperB.
if (textureInstructionSuffix != TextureInstructionSuffix.None && textureInstructionSuffix != TextureInstructionSuffix.Lz && textureTarget == GalTextureTarget.TwoD)
{
coords[coords.Length - coordStartIndex - 1] = opCode.Gpr8();
coords[coords.Length - coordStartIndex - 1].Index += coords.Length - coordStartIndex - 1;
operBIndex--;
}
// TODO: Find what MZ does and what changes about the encoding (Maybe Multisample?)
if ((textureInstructionSuffix & TextureInstructionSuffix.Ll) != 0)
{
levelOfDetail = opCode.Gpr20();
levelOfDetail.Index += operBIndex;
operBIndex++;
}
if ((textureInstructionSuffix & TextureInstructionSuffix.AOffI) != 0)
{
offset = opCode.Gpr20();
offset.Index += operBIndex;
operBIndex++;
}
if ((textureInstructionSuffix & TextureInstructionSuffix.Dc) != 0)
{
depthCompare = opCode.Gpr20();
depthCompare.Index += operBIndex;
operBIndex++;
}
int lutIndex;
lutIndex = !opCode.Gpr0().IsConst ? 1 : 0;
lutIndex |= !opCode.Gpr28().IsConst ? 2 : 0;
if (lutIndex == 0)
{
//Both destination registers are RZ, do nothing.
return;
}
bool fp16 = !opCode.Read(59);
int dstIncrement = 0;
ShaderIrOperGpr GetDst()
{
ShaderIrOperGpr dst;
if (fp16)
{
//FP16 mode, two components are packed on the two
//halfs of a 32-bits register, as two half-float values.
int halfPart = dstIncrement & 1;
switch (lutIndex)
{
case 1: dst = opCode.GprHalf0(halfPart); break;
case 2: dst = opCode.GprHalf28(halfPart); break;
case 3: dst = (dstIncrement >> 1) != 0
? opCode.GprHalf28(halfPart)
: opCode.GprHalf0(halfPart); break;
default: throw new InvalidOperationException();
}
}
else
{
//32-bits mode, each component uses one register.
//Two components uses two consecutive registers.
switch (lutIndex)
{
case 1: dst = opCode.Gpr0(); break;
case 2: dst = opCode.Gpr28(); break;
case 3: dst = (dstIncrement >> 1) != 0
? opCode.Gpr28()
: opCode.Gpr0(); break;
default: throw new InvalidOperationException();
}
dst.Index += dstIncrement & 1;
}
dstIncrement++;
return dst;
}
int chMask = _maskLut[lutIndex, opCode.Read(50, 7)];
if (chMask == 0)
{
//All channels are disabled, do nothing.
return;
}
ShaderIrNode operC = opCode.Imm13_36();
coords = CoordsRegistersToTempRegisters(block, coords);
for (int ch = 0; ch < 4; ch++)
{
if (!IsChannelUsed(chMask, ch))
{
continue;
}
ShaderIrMetaTex meta = new ShaderIrMetaTex(ch, textureTarget, textureInstructionSuffix, coords)
{
LevelOfDetail = levelOfDetail,
Offset = offset,
DepthCompare = depthCompare
};
ShaderIrOp op = new ShaderIrOp(inst, operA, operB, operC, meta);
ShaderIrOperGpr dst = GetDst();
if (dst.IsValidRegister && !dst.IsConst)
{
block.AddNode(opCode.PredNode(new ShaderIrAsg(dst, op)));
}
}
}
private static void EmitTld4(ShaderIrBlock block, long opCode, GalTextureTarget textureType, TextureInstructionSuffix textureInstructionSuffix, int chMask, int component, bool scalar)
{
ShaderIrOperGpr operA = opCode.Gpr8();
ShaderIrOperGpr operB = opCode.Gpr20();
ShaderIrOperImm operC = opCode.Imm13_36();
ShaderIrOperGpr[] coords = new ShaderIrOperGpr[ImageUtils.GetCoordsCountTextureTarget(textureType)];
ShaderIrOperGpr offset = null;
ShaderIrOperGpr depthCompare = null;
bool isArray = ImageUtils.IsArray(textureType);
int operBIndex = 0;
if (scalar)
{
int coordStartIndex = 0;
if (isArray)
{
coordStartIndex++;
coords[coords.Length - 1] = operB;
}
switch (coords.Length - coordStartIndex)
{
case 1:
coords[0] = opCode.Gpr8();
break;
case 2:
coords[0] = opCode.Gpr8();
coords[0].Index += coordStartIndex;
break;
case 3:
coords[0] = opCode.Gpr8();
coords[0].Index += coordStartIndex;
coords[1] = opCode.Gpr8();
coords[1].Index += 1 + coordStartIndex;
break;
default:
throw new NotSupportedException($"{coords.Length - coordStartIndex} coords textures aren't supported in TLD4S");
}
if (coords.Length - coordStartIndex != 1)
{
coords[coords.Length - coordStartIndex - 1] = operB;
operBIndex++;
}
if (textureInstructionSuffix != TextureInstructionSuffix.None && textureType == GalTextureTarget.TwoD)
{
coords[coords.Length - coordStartIndex - 1] = opCode.Gpr8();
coords[coords.Length - coordStartIndex - 1].Index += coords.Length - coordStartIndex - 1;
operBIndex--;
}
}
else
{
int indexExtraCoord = 0;
if (isArray)
{
indexExtraCoord++;
coords[coords.Length - 1] = opCode.Gpr8();
}
for (int index = 0; index < coords.Length - indexExtraCoord; index++)
{
coords[index] = opCode.Gpr8();
coords[index].Index += index;
coords[index].Index += indexExtraCoord;
if (coords[index].Index > ShaderIrOperGpr.ZrIndex)
{
coords[index].Index = ShaderIrOperGpr.ZrIndex;
}
}
}
if ((textureInstructionSuffix & TextureInstructionSuffix.AOffI) != 0)
{
offset = opCode.Gpr20();
offset.Index += operBIndex;
operBIndex++;
}
if ((textureInstructionSuffix & TextureInstructionSuffix.Dc) != 0)
{
depthCompare = opCode.Gpr20();
depthCompare.Index += operBIndex;
operBIndex++;
}
coords = CoordsRegistersToTempRegisters(block, coords);
int regInc = 0;
for (int ch = 0; ch < 4; ch++)
{
if (!IsChannelUsed(chMask, ch))
{
continue;
}
ShaderIrOperGpr dst = opCode.Gpr0();
dst.Index += regInc++;
if (!dst.IsValidRegister || dst.IsConst)
{
continue;
}
ShaderIrMetaTex meta = new ShaderIrMetaTex(ch, textureType, textureInstructionSuffix, coords)
{
Component = component,
Offset = offset,
DepthCompare = depthCompare
};
ShaderIrOp op = new ShaderIrOp(ShaderIrInst.Tld4, operA, operB, operC, meta);
block.AddNode(opCode.PredNode(new ShaderIrAsg(dst, op)));
}
}
private static bool IsChannelUsed(int chMask, int ch)
{
return (chMask & (1 << ch)) != 0;
}
private static ShaderIrOperGpr[] CoordsRegistersToTempRegisters(ShaderIrBlock block, params ShaderIrOperGpr[] registers)
{
ShaderIrOperGpr[] res = new ShaderIrOperGpr[registers.Length];
for (int index = 0; index < res.Length; index++)
{
res[index] = ShaderIrOperGpr.MakeTemporary(index);
block.AddNode(new ShaderIrAsg(res[index], registers[index]));
}
return res;
}
}
}