mirror of
https://github.com/ZDoom/gzdoom.git
synced 2024-11-23 04:22:34 +00:00
Merge remote-tracking branch 'remotes/origin/master' into Texture_Cleanup
# Conflicts: # src/polyrenderer/poly_renderthread.cpp # src/swrenderer/r_renderthread.cpp
This commit is contained in:
commit
91a8f5cd04
17 changed files with 126 additions and 59 deletions
|
@ -271,6 +271,22 @@ Error X86RAPass::emitSwapGp(VirtReg* dstReg, VirtReg* srcReg, uint32_t dstPhysId
|
|||
return kErrorOk;
|
||||
}
|
||||
|
||||
Error X86RAPass::emitSwapVec(VirtReg* dstReg, VirtReg* srcReg, uint32_t dstPhysId, uint32_t srcPhysId, const char* reason) noexcept {
|
||||
ASMJIT_ASSERT(dstPhysId != Globals::kInvalidRegId);
|
||||
ASMJIT_ASSERT(srcPhysId != Globals::kInvalidRegId);
|
||||
ASMJIT_ASSERT(dstPhysId != srcPhysId);
|
||||
|
||||
X86Reg a = X86Reg::fromSignature(dstReg->getSignature(), dstPhysId);
|
||||
X86Reg b = X86Reg::fromSignature(srcReg->getSignature(), srcPhysId);
|
||||
|
||||
ASMJIT_PROPAGATE(cc()->emit(X86Inst::kIdXorps, a, b));
|
||||
if (_emitComments)
|
||||
cc()->getCursor()->setInlineComment(cc()->_cbDataZone.sformat("[%s] %s, %s", reason, dstReg->getName(), srcReg->getName()));
|
||||
ASMJIT_PROPAGATE(cc()->emit(X86Inst::kIdXorps, b, a));
|
||||
ASMJIT_PROPAGATE(cc()->emit(X86Inst::kIdXorps, a, b));
|
||||
return kErrorOk;
|
||||
}
|
||||
|
||||
Error X86RAPass::emitImmToReg(uint32_t dstTypeId, uint32_t dstPhysId, const Imm* src) noexcept {
|
||||
ASMJIT_ASSERT(dstPhysId != Globals::kInvalidRegId);
|
||||
|
||||
|
@ -778,6 +794,9 @@ _MoveOrLoad:
|
|||
if (C == X86Reg::kKindGp) {
|
||||
self->swapGp(dVReg, sVd);
|
||||
}
|
||||
else if (C == X86Reg::kKindVec) {
|
||||
self->swapVec(dVReg, sVd);
|
||||
}
|
||||
else {
|
||||
self->spill<C>(dVReg);
|
||||
self->move<C>(sVd, physId);
|
||||
|
@ -932,10 +951,13 @@ static ASMJIT_INLINE void X86RAPass_intersectStateVars(X86RAPass* self, X86RASta
|
|||
didWork = true;
|
||||
continue;
|
||||
}
|
||||
else if (C == X86Reg::kKindGp) {
|
||||
else if (C == X86Reg::kKindGp || C == X86Reg::kKindVec) {
|
||||
if (aCell.getState() == VirtReg::kStateReg) {
|
||||
if (dVReg->getPhysId() != Globals::kInvalidRegId && aVReg->getPhysId() != Globals::kInvalidRegId) {
|
||||
self->swapGp(dVReg, aVReg);
|
||||
if (C == X86Reg::kKindGp)
|
||||
self->swapGp(dVReg, aVReg);
|
||||
else
|
||||
self->swapVec(dVReg, aVReg);
|
||||
|
||||
didWork = true;
|
||||
continue;
|
||||
|
@ -2787,9 +2809,13 @@ ASMJIT_INLINE void X86VarAlloc::alloc() {
|
|||
// allocation tasks by a single 'xchg' instruction, swapping
|
||||
// two registers required by the instruction/node or one register
|
||||
// required with another non-required.
|
||||
if (C == X86Reg::kKindGp && aPhysId != Globals::kInvalidRegId) {
|
||||
// Uses xor swap for Vec registers.
|
||||
if ((C == X86Reg::kKindGp || C == X86Reg::kKindVec) && aPhysId != Globals::kInvalidRegId) {
|
||||
TiedReg* bTied = bVReg->_tied;
|
||||
_context->swapGp(aVReg, bVReg);
|
||||
if (C == X86Reg::kKindGp)
|
||||
_context->swapGp(aVReg, bVReg);
|
||||
else
|
||||
_context->swapVec(aVReg, bVReg);
|
||||
|
||||
aTied->flags |= TiedReg::kRDone;
|
||||
addTiedDone(C);
|
||||
|
@ -3341,8 +3367,11 @@ ASMJIT_INLINE void X86CallAlloc::alloc() {
|
|||
// allocation tasks by a single 'xchg' instruction, swapping
|
||||
// two registers required by the instruction/node or one register
|
||||
// required with another non-required.
|
||||
if (C == X86Reg::kKindGp && sPhysId != Globals::kInvalidRegId) {
|
||||
_context->swapGp(aVReg, bVReg);
|
||||
if ((C == X86Reg::kKindGp || C == X86Reg::kKindVec) && sPhysId != Globals::kInvalidRegId) {
|
||||
if (C == X86Reg::kKindGp)
|
||||
_context->swapGp(aVReg, bVReg);
|
||||
else
|
||||
_context->swapVec(aVReg, bVReg);
|
||||
|
||||
aTied->flags |= TiedReg::kRDone;
|
||||
addTiedDone(C);
|
||||
|
|
|
@ -327,6 +327,7 @@ public:
|
|||
Error emitLoad(VirtReg* vreg, uint32_t id, const char* reason);
|
||||
Error emitSave(VirtReg* vreg, uint32_t id, const char* reason);
|
||||
Error emitSwapGp(VirtReg* aVReg, VirtReg* bVReg, uint32_t aId, uint32_t bId, const char* reason) noexcept;
|
||||
Error emitSwapVec(VirtReg* aVReg, VirtReg* bVReg, uint32_t aId, uint32_t bId, const char* reason) noexcept;
|
||||
|
||||
Error emitImmToReg(uint32_t dstTypeId, uint32_t dstPhysId, const Imm* src) noexcept;
|
||||
Error emitImmToStack(uint32_t dstTypeId, const X86Mem* dst, const Imm* src) noexcept;
|
||||
|
@ -515,6 +516,37 @@ public:
|
|||
ASMJIT_X86_CHECK_STATE
|
||||
}
|
||||
|
||||
//! Swap two registers
|
||||
//!
|
||||
//! Xor swap on Vec registers.
|
||||
ASMJIT_INLINE void swapVec(VirtReg* aVReg, VirtReg* bVReg) {
|
||||
ASMJIT_ASSERT(aVReg != bVReg);
|
||||
|
||||
ASMJIT_ASSERT(aVReg->getKind() == X86Reg::kKindVec);
|
||||
ASMJIT_ASSERT(aVReg->getState() == VirtReg::kStateReg);
|
||||
ASMJIT_ASSERT(aVReg->getPhysId() != Globals::kInvalidRegId);
|
||||
|
||||
ASMJIT_ASSERT(bVReg->getKind() == X86Reg::kKindVec);
|
||||
ASMJIT_ASSERT(bVReg->getState() == VirtReg::kStateReg);
|
||||
ASMJIT_ASSERT(bVReg->getPhysId() != Globals::kInvalidRegId);
|
||||
|
||||
uint32_t aIndex = aVReg->getPhysId();
|
||||
uint32_t bIndex = bVReg->getPhysId();
|
||||
|
||||
emitSwapVec(aVReg, bVReg, aIndex, bIndex, "Swap");
|
||||
|
||||
aVReg->setPhysId(bIndex);
|
||||
bVReg->setPhysId(aIndex);
|
||||
|
||||
_x86State.getListByKind(X86Reg::kKindVec)[aIndex] = bVReg;
|
||||
_x86State.getListByKind(X86Reg::kKindVec)[bIndex] = aVReg;
|
||||
|
||||
uint32_t m = aVReg->isModified() ^ bVReg->isModified();
|
||||
_x86State._modified.xor_(X86Reg::kKindVec, (m << aIndex) | (m << bIndex));
|
||||
|
||||
ASMJIT_X86_CHECK_STATE
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// [Alloc / Spill]
|
||||
// --------------------------------------------------------------------------
|
||||
|
|
|
@ -453,7 +453,7 @@ public:
|
|||
TObjPtr<AActor*> MUSINFOactor = nullptr; // For MUSINFO purposes
|
||||
int8_t MUSINFOtics = 0;
|
||||
|
||||
bool settings_controller = true; // Player can control game settings.
|
||||
bool settings_controller = false; // Player can control game settings.
|
||||
int8_t crouching = 0;
|
||||
int8_t crouchdir = 0;
|
||||
|
||||
|
|
|
@ -148,7 +148,7 @@ AActor* actorvalue(const svalue_t &svalue)
|
|||
return NULL;
|
||||
}
|
||||
// Inventory items in the player's inventory have to be considered non-present.
|
||||
if (svalue.value.mobj == NULL || !svalue.value.mobj->IsMapActor())
|
||||
if (SpawnedThings[intval] == nullptr || !SpawnedThings[intval]->IsMapActor())
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
|
|
@ -1285,6 +1285,7 @@ void G_PlayerReborn (int player)
|
|||
log = p->LogText;
|
||||
chasecam = p->cheats & CF_CHASECAM;
|
||||
Bot = p->Bot; //Added by MC:
|
||||
const bool settings_controller = p->settings_controller;
|
||||
|
||||
// Reset player structure to its defaults
|
||||
p->~player_t();
|
||||
|
@ -1303,6 +1304,7 @@ void G_PlayerReborn (int player)
|
|||
p->LogText = log;
|
||||
p->cheats |= chasecam;
|
||||
p->Bot = Bot; //Added by MC:
|
||||
p->settings_controller = settings_controller;
|
||||
|
||||
p->oldbuttons = ~0, p->attackdown = true; p->usedown = true; // don't do anything immediately
|
||||
p->original_oldbuttons = ~0;
|
||||
|
|
|
@ -74,6 +74,7 @@ void PolyRenderThread::FlushDrawQueue()
|
|||
}
|
||||
}
|
||||
|
||||
static std::mutex loadmutex;
|
||||
void PolyRenderThread::PrepareTexture(FSoftwareTexture *texture, FRenderStyle style)
|
||||
{
|
||||
if (texture == nullptr)
|
||||
|
@ -87,8 +88,6 @@ void PolyRenderThread::PrepareTexture(FSoftwareTexture *texture, FRenderStyle st
|
|||
// It is critical that this function is called before any direct
|
||||
// calls to GetPixels for this to work.
|
||||
|
||||
static std::mutex loadmutex;
|
||||
|
||||
std::unique_lock<std::mutex> lock(loadmutex);
|
||||
|
||||
const FSoftwareTextureSpan *spans;
|
||||
|
@ -105,10 +104,9 @@ void PolyRenderThread::PrepareTexture(FSoftwareTexture *texture, FRenderStyle st
|
|||
}
|
||||
}
|
||||
|
||||
static std::mutex polyobjmutex;
|
||||
void PolyRenderThread::PreparePolyObject(subsector_t *sub)
|
||||
{
|
||||
static std::mutex polyobjmutex;
|
||||
|
||||
std::unique_lock<std::mutex> lock(polyobjmutex);
|
||||
|
||||
if (sub->BSP == nullptr || sub->BSP->bDirty)
|
||||
|
|
|
@ -77,11 +77,14 @@ namespace
|
|||
}
|
||||
}
|
||||
|
||||
void R_ShowCurrentScaling();
|
||||
CUSTOM_CVAR(Float, vid_scalefactor, 1.0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
|
||||
{
|
||||
setsizeneeded = true;
|
||||
if (self < 0.05 || self > 2.0)
|
||||
self = 1.0;
|
||||
if (self != 1.0)
|
||||
R_ShowCurrentScaling();
|
||||
}
|
||||
|
||||
CUSTOM_CVAR(Int, vid_scalemode, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
|
||||
|
@ -142,21 +145,6 @@ void R_ShowCurrentScaling()
|
|||
Printf("Real resolution: %i x %i\nEmulated resolution: %i x %i\n", x1, y1, x2, y2);
|
||||
}
|
||||
|
||||
bool R_CalcsShouldBeBlocked()
|
||||
{
|
||||
if (vid_scalemode < 0 || vid_scalemode > 1)
|
||||
{
|
||||
Printf("vid_scalemode should be 0 or 1 before using this command.\n");
|
||||
return true;
|
||||
}
|
||||
if (vid_aspect != 0 && vid_cropaspect == true)
|
||||
{ // just warn ... I'm not going to fix this, it's a pretty niche condition anyway.
|
||||
Printf("Warning: Using this command while vid_aspect is not 0 will yield results based on FULL screen geometry, NOT cropped!.\n");
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
CCMD (vid_showcurrentscaling)
|
||||
{
|
||||
R_ShowCurrentScaling();
|
||||
|
@ -164,24 +152,21 @@ CCMD (vid_showcurrentscaling)
|
|||
|
||||
CCMD (vid_scaletowidth)
|
||||
{
|
||||
if (R_CalcsShouldBeBlocked())
|
||||
return;
|
||||
|
||||
if (argv.argc() > 1)
|
||||
vid_scalefactor = (float)((double)atof(argv[1]) / screen->GetClientWidth());
|
||||
|
||||
R_ShowCurrentScaling();
|
||||
{
|
||||
// the following enables the use of ViewportScaledWidth to get the proper dimensions in custom scale modes
|
||||
vid_scalefactor = 1;
|
||||
vid_scalefactor = (float)((double)atof(argv[1]) / ViewportScaledWidth(screen->GetClientWidth(), screen->GetClientHeight()));
|
||||
}
|
||||
}
|
||||
|
||||
CCMD (vid_scaletoheight)
|
||||
{
|
||||
if (R_CalcsShouldBeBlocked())
|
||||
return;
|
||||
|
||||
if (argv.argc() > 1)
|
||||
vid_scalefactor = (float)((double)atof(argv[1]) / screen->GetClientHeight());
|
||||
|
||||
R_ShowCurrentScaling();
|
||||
{
|
||||
vid_scalefactor = 1;
|
||||
vid_scalefactor = (float)((double)atof(argv[1]) / ViewportScaledHeight(screen->GetClientWidth(), screen->GetClientHeight()));
|
||||
}
|
||||
}
|
||||
|
||||
inline bool atob(char* I)
|
||||
|
|
|
@ -121,9 +121,9 @@ template<class T, int fill = 1> void ArrayResize(T *self, int amount)
|
|||
}
|
||||
}
|
||||
|
||||
template<class T> void ArrayReserve(T *self, int amount)
|
||||
template<class T> unsigned int ArrayReserve(T *self, int amount)
|
||||
{
|
||||
self->Reserve(amount);
|
||||
return self->Reserve(amount);
|
||||
}
|
||||
|
||||
template<class T> int ArrayMax(T *self)
|
||||
|
|
|
@ -543,6 +543,8 @@ void JitCompiler::EmitNativeCall(VMNativeFunction *target)
|
|||
ParamOpcodes.Clear();
|
||||
}
|
||||
|
||||
static std::map<FString, std::unique_ptr<TArray<uint8_t>>> argsCache;
|
||||
|
||||
asmjit::FuncSignature JitCompiler::CreateFuncSignature()
|
||||
{
|
||||
using namespace asmjit;
|
||||
|
@ -657,7 +659,6 @@ asmjit::FuncSignature JitCompiler::CreateFuncSignature()
|
|||
}
|
||||
|
||||
// FuncSignature only keeps a pointer to its args array. Store a copy of each args array variant.
|
||||
static std::map<FString, std::unique_ptr<TArray<uint8_t>>> argsCache;
|
||||
std::unique_ptr<TArray<uint8_t>> &cachedArgs = argsCache[key];
|
||||
if (!cachedArgs) cachedArgs.reset(new TArray<uint8_t>(args));
|
||||
|
||||
|
|
|
@ -169,16 +169,28 @@ void JitCompiler::EmitRET()
|
|||
if (cc.is64Bit())
|
||||
{
|
||||
if (regtype & REGT_KONST)
|
||||
cc.mov(x86::qword_ptr(location), asmjit::imm_ptr(konsta[regnum].v));
|
||||
{
|
||||
auto ptr = newTempIntPtr();
|
||||
cc.mov(ptr, asmjit::imm_ptr(konsta[regnum].v));
|
||||
cc.mov(x86::qword_ptr(location), ptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
cc.mov(x86::qword_ptr(location), regA[regnum]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (regtype & REGT_KONST)
|
||||
cc.mov(x86::dword_ptr(location), asmjit::imm_ptr(konsta[regnum].v));
|
||||
{
|
||||
auto ptr = newTempIntPtr();
|
||||
cc.mov(ptr, asmjit::imm_ptr(konsta[regnum].v));
|
||||
cc.mov(x86::dword_ptr(location), ptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
cc.mov(x86::dword_ptr(location), regA[regnum]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -2451,7 +2451,7 @@ DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, GetAutomapPosition, GetAutomapPositi
|
|||
ACTION_RETURN_VEC2(AM_GetPosition());
|
||||
}
|
||||
|
||||
static int ZGetUDMFInt(int type, int index, int key)
|
||||
static int ZGetUDMFInt(FLevelLocals *self, int type, int index, int key)
|
||||
{
|
||||
return GetUDMFInt(type, index, ENamedName(key));
|
||||
}
|
||||
|
@ -2465,7 +2465,7 @@ DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, GetUDMFInt, ZGetUDMFInt)
|
|||
ACTION_RETURN_INT(GetUDMFInt(type, index, key));
|
||||
}
|
||||
|
||||
static double ZGetUDMFFloat(int type, int index, int key)
|
||||
static double ZGetUDMFFloat(FLevelLocals *self, int type, int index, int key)
|
||||
{
|
||||
return GetUDMFFloat(type, index, ENamedName(key));
|
||||
}
|
||||
|
@ -2479,7 +2479,7 @@ DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, GetUDMFFloat, ZGetUDMFFloat)
|
|||
ACTION_RETURN_FLOAT(GetUDMFFloat(type, index, key));
|
||||
}
|
||||
|
||||
static void ZGetUDMFString(int type, int index, int key, FString *result)
|
||||
static void ZGetUDMFString(FLevelLocals *self, int type, int index, int key, FString *result)
|
||||
{
|
||||
*result = GetUDMFString(type, index, ENamedName(key));
|
||||
}
|
||||
|
|
|
@ -128,7 +128,12 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetPointer, COPY_AAPTR)
|
|||
//
|
||||
//==========================================================================
|
||||
|
||||
DEFINE_ACTION_FUNCTION_NATIVE(AActor, A_StopSound, S_StopSound)
|
||||
static void NativeStopSound(AActor *actor, int slot)
|
||||
{
|
||||
S_StopSound(actor, slot);
|
||||
}
|
||||
|
||||
DEFINE_ACTION_FUNCTION_NATIVE(AActor, A_StopSound, NativeStopSound)
|
||||
{
|
||||
PARAM_SELF_PROLOGUE(AActor);
|
||||
PARAM_INT(slot);
|
||||
|
@ -475,7 +480,7 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, Vec2To, Vec2To)
|
|||
ACTION_RETURN_VEC2(self->Vec2To(t));
|
||||
}
|
||||
|
||||
static void Vec3Angle(AActor *self, double length, double angle, double z, bool absolute, DVector2 *result)
|
||||
static void Vec3Angle(AActor *self, double length, double angle, double z, bool absolute, DVector3 *result)
|
||||
{
|
||||
*result = self->Vec3Angle(length, angle, z, absolute);
|
||||
}
|
||||
|
|
|
@ -89,8 +89,8 @@ namespace swrenderer
|
|||
return pal_drawers.get();
|
||||
}
|
||||
|
||||
void RenderThread::PrepareTexture(FSoftwareTexture *texture, FRenderStyle style)
|
||||
{
|
||||
static std::mutex loadmutex;
|
||||
void RenderThread::PrepareTexture(FSoftwareTexture *texture, FRenderStyle style) {
|
||||
if (texture == nullptr)
|
||||
return;
|
||||
|
||||
|
@ -102,8 +102,6 @@ namespace swrenderer
|
|||
// It is critical that this function is called before any direct
|
||||
// calls to GetPixels for this to work.
|
||||
|
||||
static std::mutex loadmutex;
|
||||
|
||||
std::unique_lock<std::mutex> lock(loadmutex);
|
||||
|
||||
const FSoftwareTextureSpan *spans;
|
||||
|
@ -117,13 +115,12 @@ namespace swrenderer
|
|||
bool alpha = !!(style.Flags & STYLEF_RedIsAlpha);
|
||||
texture->GetPixels(alpha);
|
||||
texture->GetColumn(alpha, 0, &spans);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static std::mutex polyobjmutex;
|
||||
void RenderThread::PreparePolyObject(subsector_t *sub)
|
||||
{
|
||||
static std::mutex polyobjmutex;
|
||||
|
||||
std::unique_lock<std::mutex> lock(polyobjmutex);
|
||||
|
||||
if (sub->BSP == nullptr || sub->BSP->bDirty)
|
||||
|
|
|
@ -70,12 +70,12 @@ TArray<FSWColormap> SpecialSWColormaps;
|
|||
// Colored Lighting Stuffs
|
||||
//
|
||||
//==========================================================================
|
||||
static std::mutex buildmapmutex;
|
||||
|
||||
static FDynamicColormap *CreateSpecialLights (PalEntry color, PalEntry fade, int desaturate)
|
||||
{
|
||||
// GetSpecialLights is called by the scene worker threads.
|
||||
// If we didn't find the colormap, search again, but this time one thread at a time
|
||||
static std::mutex buildmapmutex;
|
||||
std::unique_lock<std::mutex> lock(buildmapmutex);
|
||||
|
||||
// If this colormap has already been created, just return it
|
||||
|
|
|
@ -219,8 +219,6 @@ void DFrameBuffer::DrawTextCommon(FFont *font, int normalcolor, double x, double
|
|||
int kerning;
|
||||
FTexture *pic;
|
||||
|
||||
assert(string[0] != '$');
|
||||
|
||||
if (parms.celly == 0) parms.celly = font->GetHeight() + 1;
|
||||
parms.celly *= parms.scaley;
|
||||
|
||||
|
|
|
@ -93,6 +93,11 @@ CUSTOM_CVAR(Int, vid_maxfps, 200, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
|
|||
|
||||
CUSTOM_CVAR(Int, vid_rendermode, 4, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_NOINITCALL)
|
||||
{
|
||||
if (self < 0 || self > 4)
|
||||
{
|
||||
self = 4;
|
||||
}
|
||||
|
||||
if (usergame)
|
||||
{
|
||||
// [SP] Update pitch limits to the netgame/gamesim.
|
||||
|
|
|
@ -172,6 +172,9 @@ AF40D0E49BD1B76D4B1AADD8212ADC46 // MAP01 (the wad that shall not be named =P)
|
|||
3DEE4EFEFAF3260C800A30734F54CE75 // Hellbound, map14
|
||||
5FAA25F5A6AAB3409CAE0AF87F910341 // DOOM.wad e1m6
|
||||
94893A0DC429A22ADC4B3A73DA537E16 // DOOM2.WAD map25
|
||||
D5F64E02679A81B82006AF34A6A8EAC3 // plutonia.wad map32
|
||||
BA4860C7A2F5D705DB32A1A38DB77EC4 // pl2.wad map10
|
||||
EDA5CE7C462BD171BF8110AC56B67857 // pl2.wad map11
|
||||
{
|
||||
rebuildnodes
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue