mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
71 commits from474ad59818to2cf8dabe6b, triaged rather than taken wholesale. Declined, resolved to ours: - AGENTS.md: upstream's AI-agent instructions; we carry our own and do not want a second, conflicting policy file. - CI deps bump (setup-node, labeler): both target workflows are absent here, and the labeler job is gated on the repository being PCSX2/pcsx2. - KDDockWidgets 2.4.1: two of the six files do not exist here; we already build 2.4.0 against a 2.3.0 floor, so there is nothing to gain. - The FullscreenUI Achievements-layout realignment: our section already carries the same settings, and ours is the branded copy. - The GS draw/vertex-buffer cluster (7887919e74,b2fa00844e,99cfbb49c1,5c611f85e1,9945046a49,af48193ebb,d88510e3a6,8c1bb5742e). Our vertex kick is an ARM64 rewrite of the same hot path -- register-resident cursor, fused min/max with a rewind watermark, and a scalar cull mirror that dual-issues against the NEON parse -- so upstream's generic pointer-logic optimisation is a variant of work already banked here, and their growth restructure replaces per-buffer capacity with a single global value, which the pooled draw-node model cannot express. Two of the four August commits in that cluster repair regressions the July rewrite introduced, and the third's genuine fix (staging arrays sized from an unrelated buffer) we had already made independently. Taken with adjustment: - EATAN coefficients (aae9438f98). Upstream relabelled mVU_Globals so the names match the powers; we had fixed the same defect by ordering the arm64 call sites by power instead. Both fixes are correct alone and CANCEL when combined, so the arm64 call sites move to plain ascending order in the same commit. The values never moved, so this emits an identical instruction sequence. Their fix also repairs the x86 mVU we still carry. - Shader cache version: upstream numbered their tfx.glsl change 109, which is below our 110. Taking their value would hand every user a stale blob, so this lands as 111. - FullscreenUI: took the two readback-spin toggles, placed outside our non-Apple guard rather than inside upstream's unguarded run. - Restored tools/generate_fullscreen_ui_translation_strings.py, dropped by431ca0c063, and regenerated both string areas. That also registers the Big Picture setup-wizard strings, which had never been extractable. GameDB: the three serials upstream gave gsHWFixes (SLES-53869, PAPX-90020, SCPS-15064) are absent from the mobile overlay, so no fix is silently erased on handhelds.
975 lines
25 KiB
C++
975 lines
25 KiB
C++
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
|
|
// SPDX-License-Identifier: GPL-3.0+
|
|
|
|
#include "GS/GSClut.h"
|
|
#include "GS/GSExtra.h"
|
|
#include "GS/GSLocalMemory.h"
|
|
#include "GS/GSGL.h"
|
|
#include "GS/GSUtil.h"
|
|
#include "GS/Renderers/Common/GSDevice.h"
|
|
#include "GS/Renderers/Common/GSRenderer.h"
|
|
#include "common/AlignedMalloc.h"
|
|
#include "common/Console.h"
|
|
|
|
GSClut::GSClut(GSLocalMemory* mem)
|
|
: m_mem(mem)
|
|
{
|
|
// 1k + 1k for mirrored area simulating wrapping memory
|
|
m_clut = static_cast<u16*>(_aligned_malloc(CLUT_ALLOC_SIZE, VECTOR_ALIGNMENT));
|
|
if (!m_clut)
|
|
pxFailRel("Failed to allocate CLUT storage.");
|
|
|
|
m_buff32 = reinterpret_cast<u32*>(reinterpret_cast<u8*>(m_clut) + 2048); // 1k
|
|
m_buff64 = reinterpret_cast<u64*>(reinterpret_cast<u8*>(m_clut) + 4096); // 2k
|
|
m_write.dirty = 1;
|
|
m_read.dirty = true;
|
|
|
|
for (int i = 0; i < 16; i++)
|
|
{
|
|
for (int j = 0; j < 64; j++)
|
|
{
|
|
// The GS seems to check the lower 3 bits to tell if the format is 8/4bit
|
|
// for the reload.
|
|
const bool eight_bit = (j & 0x7) == 0x3;
|
|
const bool four_bit = (j & 0x7) == 0x4;
|
|
|
|
switch (i)
|
|
{
|
|
case PSMCT32:
|
|
case PSMCT24: // undocumented (KH?)
|
|
if (eight_bit)
|
|
m_wc[0][i][j] = &GSClut::WriteCLUT32_I8_CSM1;
|
|
else if (four_bit)
|
|
m_wc[0][i][j] = &GSClut::WriteCLUT32_I4_CSM1;
|
|
else
|
|
m_wc[0][i][j] = &GSClut::WriteCLUT_NULL;
|
|
break;
|
|
case PSMCT16:
|
|
if (eight_bit)
|
|
m_wc[0][i][j] = &GSClut::WriteCLUT16_I8_CSM1;
|
|
else if (four_bit)
|
|
m_wc[0][i][j] = &GSClut::WriteCLUT16_I4_CSM1;
|
|
else
|
|
m_wc[0][i][j] = &GSClut::WriteCLUT_NULL;
|
|
break;
|
|
case PSMCT16S:
|
|
if (eight_bit)
|
|
m_wc[0][i][j] = &GSClut::WriteCLUT16S_I8_CSM1;
|
|
else if (four_bit)
|
|
m_wc[0][i][j] = &GSClut::WriteCLUT16S_I4_CSM1;
|
|
else
|
|
m_wc[0][i][j] = &GSClut::WriteCLUT_NULL;
|
|
break;
|
|
default:
|
|
m_wc[0][i][j] = &GSClut::WriteCLUT_NULL;
|
|
}
|
|
|
|
// TODO: test this
|
|
m_wc[1][i][j] = &GSClut::WriteCLUT_NULL;
|
|
}
|
|
}
|
|
|
|
m_wc[1][PSMCT32][PSMT8] = &GSClut::WriteCLUT32_CSM2<256>;
|
|
m_wc[1][PSMCT32][PSMT8H] = &GSClut::WriteCLUT32_CSM2<256>;
|
|
m_wc[1][PSMCT32][PSMT4] = &GSClut::WriteCLUT32_CSM2<16>;
|
|
m_wc[1][PSMCT32][PSMT4HL] = &GSClut::WriteCLUT32_CSM2<16>;
|
|
m_wc[1][PSMCT32][PSMT4HH] = &GSClut::WriteCLUT32_CSM2<16>;
|
|
m_wc[1][PSMCT24][PSMT8] = &GSClut::WriteCLUT32_CSM2<256>;
|
|
m_wc[1][PSMCT24][PSMT8H] = &GSClut::WriteCLUT32_CSM2<256>;
|
|
m_wc[1][PSMCT24][PSMT4] = &GSClut::WriteCLUT32_CSM2<16>;
|
|
m_wc[1][PSMCT24][PSMT4HL] = &GSClut::WriteCLUT32_CSM2<16>;
|
|
m_wc[1][PSMCT24][PSMT4HH] = &GSClut::WriteCLUT32_CSM2<16>;
|
|
m_wc[1][PSMCT16][PSMT8] = &GSClut::WriteCLUT16_CSM2<256>;
|
|
m_wc[1][PSMCT16][PSMT8H] = &GSClut::WriteCLUT16_CSM2<256>;
|
|
m_wc[1][PSMCT16][PSMT4] = &GSClut::WriteCLUT16_CSM2<16>;
|
|
m_wc[1][PSMCT16][PSMT4HL] = &GSClut::WriteCLUT16_CSM2<16>;
|
|
m_wc[1][PSMCT16][PSMT4HH] = &GSClut::WriteCLUT16_CSM2<16>;
|
|
m_wc[1][PSMCT16S][PSMT8] = &GSClut::WriteCLUT16S_CSM2<256>;
|
|
m_wc[1][PSMCT16S][PSMT8H] = &GSClut::WriteCLUT16S_CSM2<256>;
|
|
m_wc[1][PSMCT16S][PSMT4] = &GSClut::WriteCLUT16S_CSM2<16>;
|
|
m_wc[1][PSMCT16S][PSMT4HL] = &GSClut::WriteCLUT16S_CSM2<16>;
|
|
m_wc[1][PSMCT16S][PSMT4HH] = &GSClut::WriteCLUT16S_CSM2<16>;
|
|
}
|
|
|
|
GSClut::~GSClut()
|
|
{
|
|
delete m_gpu_clut4;
|
|
delete m_gpu_clut8;
|
|
|
|
_aligned_free(m_clut);
|
|
}
|
|
|
|
u8 GSClut::IsInvalid()
|
|
{
|
|
return m_write.dirty;
|
|
}
|
|
|
|
void GSClut::ClearDrawInvalidity()
|
|
{
|
|
if (m_write.dirty & 2)
|
|
{
|
|
m_write.dirty = 1;
|
|
}
|
|
}
|
|
|
|
u32 GSClut::GetCLUTCBP()
|
|
{
|
|
return m_write.TEX0.CBP;
|
|
}
|
|
|
|
u32 GSClut::GetCLUTCPSM()
|
|
{
|
|
return m_write.TEX0.CPSM;
|
|
}
|
|
|
|
void GSClut::SetNextCLUTTEX0(u64 TEX0)
|
|
{
|
|
m_write.next_tex0 = TEX0;
|
|
}
|
|
|
|
void GSClut::Reset()
|
|
{
|
|
std::memset(m_CBP, 0, sizeof(m_CBP));
|
|
std::memset(m_clut, 0, CLUT_ALLOC_SIZE);
|
|
m_write = {};
|
|
m_write.dirty = 1;
|
|
m_read = {};
|
|
m_read.dirty = true;
|
|
}
|
|
|
|
bool GSClut::InvalidateRange(u32 start_block, u32 end_block, bool is_draw)
|
|
{
|
|
if (m_write.dirty & 2)
|
|
return m_write.dirty;
|
|
|
|
GIFRegTEX0 next_cbp;
|
|
next_cbp.U64 = m_write.next_tex0;
|
|
|
|
// Handle wrapping writes. Star Wars Battlefront 2 does this.
|
|
if ((end_block & 0xFFE0) < (start_block & 0xFFE0))
|
|
{
|
|
if ((next_cbp.CBP + 3U) <= end_block)
|
|
next_cbp.CBP += 0x4000;
|
|
|
|
end_block += 0x4000;
|
|
}
|
|
|
|
if ((next_cbp.CBP + 3U) >= start_block && end_block >= next_cbp.CBP)
|
|
{
|
|
m_write.dirty |= is_draw ? 2 : 1;
|
|
}
|
|
|
|
return m_write.dirty;
|
|
}
|
|
|
|
bool GSClut::CanLoadCLUT(const GIFRegTEX0& TEX0, const bool update_CBP)
|
|
{
|
|
if ((TEX0.PSM & 0x7) < 3)
|
|
return false;
|
|
|
|
switch (TEX0.CLD)
|
|
{
|
|
case 0:
|
|
case 6: // FFX2 menu.
|
|
case 7: // Ford Mustang Racing, Bouken Jidai Katsugeki Goemon.
|
|
return false;
|
|
case 1:
|
|
break;
|
|
case 2:
|
|
if (update_CBP)
|
|
m_CBP[0] = TEX0.CBP;
|
|
break;
|
|
case 3:
|
|
if (update_CBP)
|
|
m_CBP[1] = TEX0.CBP;
|
|
break;
|
|
case 4:
|
|
if (m_CBP[0] == TEX0.CBP)
|
|
return false;
|
|
if (update_CBP)
|
|
m_CBP[0] = TEX0.CBP;
|
|
break;
|
|
case 5:
|
|
if (m_CBP[1] == TEX0.CBP)
|
|
return false;
|
|
if (update_CBP)
|
|
m_CBP[1] = TEX0.CBP;
|
|
break;
|
|
default:
|
|
ASSUME(0);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GSClut::WriteTest(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
|
|
{
|
|
// Check if PSM is an indexed format BEFORE the load condition, updating CBP0/1 on an invalid format is not allowed
|
|
// and can break games. Corvette (NTSC) is a good example of this.
|
|
if ((TEX0.PSM & 0x7) < 3)
|
|
return false;
|
|
|
|
if (!CanLoadCLUT(TEX0, true))
|
|
return false;
|
|
|
|
// CLUT only reloads if PSM is a valid index type, avoid unnecessary flushes.
|
|
return m_write.IsDirty(TEX0, TEXCLUT);
|
|
}
|
|
|
|
void GSClut::WriteDecision(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
|
|
{
|
|
m_write.TEX0 = TEX0;
|
|
m_write.TEXCLUT = TEXCLUT;
|
|
m_write.dirty = 0;
|
|
}
|
|
|
|
void GSClut::WriteLoad(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
|
|
{
|
|
m_read.dirty = true;
|
|
|
|
(this->*m_wc[TEX0.CSM][TEX0.CPSM][TEX0.PSM])(TEX0, TEXCLUT);
|
|
}
|
|
|
|
void GSClut::WriteCLUT32_I8_CSM1(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
|
|
{
|
|
ALIGN_STACK(32);
|
|
WriteCLUT_T32_I8_CSM1((u32*)m_mem->BlockPtr32(0, 0, TEX0.CBP, 1), m_clut, (TEX0.CSA & 15));
|
|
}
|
|
|
|
void GSClut::WriteCLUT32_I4_CSM1(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
|
|
{
|
|
ALIGN_STACK(32);
|
|
|
|
WriteCLUT_T32_I4_CSM1((u32*)m_mem->BlockPtr32(0, 0, TEX0.CBP, 1), m_clut + ((TEX0.CSA & 15) << 4));
|
|
}
|
|
|
|
void GSClut::WriteCLUT16_I8_CSM1(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
|
|
{
|
|
WriteCLUT_T16_I8_CSM1((u16*)m_mem->BlockPtr16(0, 0, TEX0.CBP, 1), m_clut + (TEX0.CSA << 4));
|
|
}
|
|
|
|
void GSClut::WriteCLUT16_I4_CSM1(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
|
|
{
|
|
WriteCLUT_T16_I4_CSM1((u16*)m_mem->BlockPtr16(0, 0, TEX0.CBP, 1), m_clut + (TEX0.CSA << 4));
|
|
}
|
|
|
|
void GSClut::WriteCLUT16S_I8_CSM1(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
|
|
{
|
|
WriteCLUT_T16_I8_CSM1((u16*)m_mem->BlockPtr16S(0, 0, TEX0.CBP, 1), m_clut + (TEX0.CSA << 4));
|
|
}
|
|
|
|
void GSClut::WriteCLUT16S_I4_CSM1(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
|
|
{
|
|
WriteCLUT_T16_I4_CSM1((u16*)m_mem->BlockPtr16S(0, 0, TEX0.CBP, 1), m_clut + (TEX0.CSA << 4));
|
|
}
|
|
|
|
template <int n>
|
|
void GSClut::WriteCLUT32_CSM2(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
|
|
{
|
|
GSOffset off = GSOffset::fromKnownPSM(TEX0.CBP, TEXCLUT.CBW, PSMCT32);
|
|
GSOffset::PAHelper pa = off.paMulti(TEXCLUT.COU << 4, TEXCLUT.COV);
|
|
|
|
u32* vm = m_mem->vm32();
|
|
u16* RESTRICT clut = m_clut + ((TEX0.CSA & 15) << 4);
|
|
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
u32 c = vm[pa.value(i)];
|
|
|
|
clut[i] = (u16)(c & 0xffff);
|
|
clut[i + 256] = (u16)(c >> 16);
|
|
}
|
|
}
|
|
|
|
template <int n>
|
|
void GSClut::WriteCLUT16_CSM2(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
|
|
{
|
|
const GSOffset off = GSOffset::fromKnownPSM(TEX0.CBP, TEXCLUT.CBW, PSMCT16);
|
|
const GSOffset::PAHelper pa = off.paMulti(TEXCLUT.COU << 4, TEXCLUT.COV);
|
|
|
|
u16* vm = m_mem->vm16();
|
|
u16* RESTRICT clut = m_clut + (TEX0.CSA << 4);
|
|
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
clut[i] = vm[pa.value(i)];
|
|
}
|
|
}
|
|
|
|
template <int n>
|
|
void GSClut::WriteCLUT16S_CSM2(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
|
|
{
|
|
const GSOffset off = GSOffset::fromKnownPSM(TEX0.CBP, TEXCLUT.CBW, PSMCT16S);
|
|
const GSOffset::PAHelper pa = off.paMulti(TEXCLUT.COU << 4, TEXCLUT.COV);
|
|
|
|
u16* vm = m_mem->vm16();
|
|
u16* RESTRICT clut = m_clut + (TEX0.CSA << 4);
|
|
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
clut[i] = vm[pa.value(i)];
|
|
}
|
|
}
|
|
|
|
void GSClut::WriteCLUT_NULL(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
|
|
{
|
|
// xenosaga3, bios
|
|
GL_INS("[WARNING] CLUT write ignored (psm: %d, cpsm: %d)", TEX0.PSM, TEX0.CPSM);
|
|
}
|
|
|
|
#if 0
|
|
void GSClut::Read(const GIFRegTEX0& TEX0)
|
|
{
|
|
if(m_read.IsDirty(TEX0))
|
|
{
|
|
m_read.TEX0 = TEX0;
|
|
m_read.dirty = false;
|
|
|
|
u16* clut = m_clut;
|
|
|
|
if(TEX0.CPSM == PSMCT32 || TEX0.CPSM == PSMCT24)
|
|
{
|
|
switch(TEX0.PSM)
|
|
{
|
|
case PSMT8:
|
|
case PSMT8H:
|
|
clut += (TEX0.CSA & 15) << 4;
|
|
ReadCLUT_T32_I8(clut, m_buff32);
|
|
break;
|
|
case PSMT4:
|
|
case PSMT4HL:
|
|
case PSMT4HH:
|
|
clut += (TEX0.CSA & 15) << 4;
|
|
ReadCLUT_T32_I4(clut, m_buff32, m_buff64);
|
|
break;
|
|
}
|
|
}
|
|
else if (TEX0.CPSM == PSMCT16 || TEX0.CPSM == PSMCT16S)
|
|
{
|
|
switch(TEX0.PSM)
|
|
{
|
|
case PSMT8:
|
|
case PSMT8H:
|
|
clut += TEX0.CSA << 4;
|
|
ReadCLUT_T16_I8(clut, m_buff32);
|
|
break;
|
|
case PSMT4:
|
|
case PSMT4HL:
|
|
case PSMT4HH:
|
|
clut += TEX0.CSA << 4;
|
|
ReadCLUT_T16_I4(clut, m_buff32, m_buff64);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void GSClut::Read32(const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA)
|
|
{
|
|
if (m_read.IsDirty(TEX0, TEXA))
|
|
{
|
|
m_read.TEX0 = TEX0;
|
|
m_read.TEXA = TEXA;
|
|
m_read.dirty = false;
|
|
m_read.adirty = true;
|
|
|
|
u16* clut = m_clut;
|
|
|
|
if (TEX0.CPSM == PSMCT32 || TEX0.CPSM == PSMCT24)
|
|
{
|
|
switch (TEX0.PSM)
|
|
{
|
|
case PSMT8:
|
|
case PSMT8H:
|
|
ReadCLUT_T32_I8(clut, m_buff32, (TEX0.CSA & 15) << 4);
|
|
break;
|
|
case PSMT4:
|
|
case PSMT4HL:
|
|
case PSMT4HH:
|
|
clut += (TEX0.CSA & 15) << 4;
|
|
// TODO: merge these functions
|
|
|
|
// This is for a situation with Breath of Fire Dragon Quarter where it reinterprets the buffer under CSM2 after reading as CSM1.
|
|
if (TEX0.CSM == 1 && m_write.TEX0.CSM == 0)
|
|
ReadCLUT_T32_I4_Swizzled(clut, m_buff32);
|
|
else
|
|
ReadCLUT_T32_I4(clut, m_buff32);
|
|
|
|
ExpandCLUT64_T32_I8(m_buff32, (u64*)m_buff64); // sw renderer does not need m_buff64 anymore
|
|
break;
|
|
}
|
|
}
|
|
else if (TEX0.CPSM == PSMCT16 || TEX0.CPSM == PSMCT16S)
|
|
{
|
|
switch (TEX0.PSM)
|
|
{
|
|
case PSMT8:
|
|
case PSMT8H:
|
|
clut += TEX0.CSA << 4;
|
|
Expand16(clut, m_buff32, 256, TEXA);
|
|
break;
|
|
case PSMT4:
|
|
case PSMT4HL:
|
|
case PSMT4HH:
|
|
clut += TEX0.CSA << 4;
|
|
// TODO: merge these functions
|
|
Expand16(clut, m_buff32, 16, TEXA);
|
|
ExpandCLUT64_T32_I8(m_buff32, (u64*)m_buff64); // sw renderer does not need m_buff64 anymore
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (GSConfig.UserHacks_GPUTargetCLUTMode != GSGPUTargetCLUTMode::Disabled)
|
|
{
|
|
const bool is_4bit = (TEX0.PSM == PSMT4 || TEX0.PSM == PSMT4HL || TEX0.PSM == PSMT4HH);
|
|
|
|
u32 CBW;
|
|
GSVector2i offset;
|
|
GSVector2i size;
|
|
float scale;
|
|
if (!TEX0.CSM)
|
|
{
|
|
CBW = 0; // don't care
|
|
offset = {};
|
|
size.x = is_4bit ? 8 : 16;
|
|
size.y = is_4bit ? 2 : 16;
|
|
}
|
|
else
|
|
{
|
|
CBW = m_write.TEXCLUT.CBW;
|
|
offset.x = m_write.TEXCLUT.COU;
|
|
offset.y = m_write.TEXCLUT.COV;
|
|
size.x = is_4bit ? 16 : 256;
|
|
size.y = 1;
|
|
}
|
|
|
|
GSTexture* src = g_gs_renderer->LookupPaletteSource(TEX0.CBP, TEX0.CPSM, CBW, offset, &scale, size);
|
|
if (src)
|
|
{
|
|
GSTexture* dst = is_4bit ? m_gpu_clut4 : m_gpu_clut8;
|
|
const u32 dst_size = is_4bit ? 16 : 256;
|
|
const u32 dOffset = (TEX0.CSA & ((TEX0.CPSM == PSMCT16 || TEX0.CPSM == PSMCT16S) ? 15u : 31u)) << 4;
|
|
|
|
if (src != m_current_gpu_clut && (src != m_last_gpu_clut || m_gpu_clut_last_offset != offset))
|
|
m_gpu_clut_dirty = true;
|
|
|
|
if (!dst)
|
|
{
|
|
// allocate texture lazily
|
|
dst = g_gs_device->CreateFeedbackTarget(dst_size, 1, GSTexture::Format::Color, false);
|
|
is_4bit ? (m_gpu_clut4 = dst) : (m_gpu_clut8 = dst);
|
|
m_gpu_clut_dirty = true;
|
|
}
|
|
if (dst)
|
|
{
|
|
GL_PUSH("Update GPU CLUT [CBP=%04X, CPSM=%s, CBW=%u, CSA=%u, Offset=(%d,%d)]",
|
|
TEX0.CBP, GSUtil::GetPSMName(TEX0.CPSM), CBW, TEX0.CSA, offset.x, offset.y);
|
|
|
|
if (m_gpu_clut_dirty)
|
|
{
|
|
m_last_gpu_clut = src;
|
|
g_gs_device->UpdateCLUTTexture(src, scale, offset.x, offset.y, dst, dOffset, dst_size);
|
|
}
|
|
|
|
m_gpu_clut_dirty = false;
|
|
m_current_gpu_clut = dst;
|
|
m_gpu_clut_last_offset = offset;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
m_current_gpu_clut = nullptr;
|
|
}
|
|
}
|
|
|
|
void GSClut::GetAlphaMinMax32(int& amin_out, int& amax_out)
|
|
{
|
|
// call only after Read32
|
|
|
|
if (m_read.dirty)
|
|
GL_INS("GSClut: GetAlphaMinMax32 m_read.dirty");
|
|
|
|
if (m_read.adirty)
|
|
{
|
|
m_read.adirty = false;
|
|
|
|
if (GSLocalMemory::m_psm[m_read.TEX0.CPSM].trbpp == 24 && m_read.TEXA.AEM == 0)
|
|
{
|
|
m_read.amin = m_read.TEXA.TA0;
|
|
m_read.amax = m_read.TEXA.TA0;
|
|
}
|
|
else
|
|
{
|
|
const GSVector4i* p = (const GSVector4i*)m_buff32;
|
|
|
|
GSVector4i amin, amax;
|
|
|
|
if (GSLocalMemory::m_psm[m_read.TEX0.PSM].pal == 256)
|
|
{
|
|
amin = GSVector4i::xffffffff();
|
|
amax = GSVector4i::zero();
|
|
|
|
for (int i = 0; i < 16; i++)
|
|
{
|
|
GSVector4i v0 = (p[i * 4 + 0] >> 24).ps32(p[i * 4 + 1] >> 24);
|
|
GSVector4i v1 = (p[i * 4 + 2] >> 24).ps32(p[i * 4 + 3] >> 24);
|
|
GSVector4i v2 = v0.pu16(v1);
|
|
|
|
amin = amin.min_u8(v2);
|
|
amax = amax.max_u8(v2);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pxAssert(GSLocalMemory::m_psm[m_read.TEX0.PSM].pal == 16);
|
|
|
|
const GSVector4i v0 = (p[0] >> 24).ps32(p[1] >> 24);
|
|
const GSVector4i v1 = (p[2] >> 24).ps32(p[3] >> 24);
|
|
const GSVector4i v2 = v0.pu16(v1);
|
|
|
|
amin = v2;
|
|
amax = v2;
|
|
}
|
|
|
|
amin = amin.min_u8(amin.zwxy());
|
|
amax = amax.max_u8(amax.zwxy());
|
|
amin = amin.min_u8(amin.zwxyl());
|
|
amax = amax.max_u8(amax.zwxyl());
|
|
amin = amin.min_u8(amin.yxwzl());
|
|
amax = amax.max_u8(amax.yxwzl());
|
|
|
|
const GSVector4i v0 = amin.upl8(amax).u8to16();
|
|
const GSVector4i v1 = v0.yxwz();
|
|
|
|
m_read.amin = v0.min_i16(v1).extract16<0>();
|
|
m_read.amax = v0.max_i16(v1).extract16<1>();
|
|
}
|
|
}
|
|
|
|
amin_out = m_read.amin;
|
|
amax_out = m_read.amax;
|
|
}
|
|
|
|
//
|
|
|
|
void GSClut::WriteCLUT_T32_I8_CSM1(const u32* RESTRICT src, u16* RESTRICT clut, u16 offset)
|
|
{
|
|
// This is required when CSA is offset from the base of the CLUT so we point to the right data
|
|
for (int i = offset; i < 16; i++)
|
|
{
|
|
const int off = i << 4; // WriteCLUT_T32_I4_CSM1 loads 16 at a time
|
|
// Source column
|
|
const int s = clutTableT32I8[off & 0x70] | (off & 0x80);
|
|
|
|
WriteCLUT_T32_I4_CSM1(&src[s], &clut[off]);
|
|
}
|
|
}
|
|
|
|
__forceinline void GSClut::WriteCLUT_T32_I4_CSM1(const u32* RESTRICT src, u16* RESTRICT clut)
|
|
{
|
|
// 1 block
|
|
|
|
#if _M_SSE >= 0x501
|
|
|
|
GSVector8i* s = (GSVector8i*)src;
|
|
GSVector8i* d = (GSVector8i*)clut;
|
|
|
|
GSVector8i v0 = s[0].acbd();
|
|
GSVector8i v1 = s[1].acbd();
|
|
|
|
GSVector8i::sw16(v0, v1);
|
|
GSVector8i::sw16(v0, v1);
|
|
GSVector8i::sw16(v0, v1);
|
|
|
|
d[0] = v0;
|
|
d[16] = v1;
|
|
|
|
#else
|
|
|
|
GSVector4i* s = (GSVector4i*)src;
|
|
GSVector4i* d = (GSVector4i*)clut;
|
|
|
|
GSVector4i v0 = s[0];
|
|
GSVector4i v1 = s[1];
|
|
GSVector4i v2 = s[2];
|
|
GSVector4i v3 = s[3];
|
|
|
|
GSVector4i::sw16(v0, v1, v2, v3);
|
|
GSVector4i::sw32(v0, v1, v2, v3);
|
|
GSVector4i::sw16(v0, v2, v1, v3);
|
|
|
|
d[0] = v0;
|
|
d[1] = v2;
|
|
d[32] = v1;
|
|
d[33] = v3;
|
|
|
|
#endif
|
|
}
|
|
|
|
void GSClut::WriteCLUT_T16_I8_CSM1(const u16* RESTRICT src, u16* RESTRICT clut)
|
|
{
|
|
// 2 blocks
|
|
|
|
GSVector4i* s = (GSVector4i*)src;
|
|
GSVector4i* d = (GSVector4i*)clut;
|
|
|
|
for (int i = 0; i < 32; i += 4)
|
|
{
|
|
GSVector4i v0 = s[i + 0];
|
|
GSVector4i v1 = s[i + 1];
|
|
GSVector4i v2 = s[i + 2];
|
|
GSVector4i v3 = s[i + 3];
|
|
|
|
GSVector4i::sw16(v0, v1, v2, v3);
|
|
GSVector4i::sw32(v0, v1, v2, v3);
|
|
GSVector4i::sw16(v0, v2, v1, v3);
|
|
|
|
d[i + 0] = v0;
|
|
d[i + 1] = v2;
|
|
d[i + 2] = v1;
|
|
d[i + 3] = v3;
|
|
}
|
|
}
|
|
|
|
__forceinline void GSClut::WriteCLUT_T16_I4_CSM1(const u16* RESTRICT src, u16* RESTRICT clut)
|
|
{
|
|
// 1 block (half)
|
|
|
|
for (int i = 0; i < 16; i++)
|
|
{
|
|
clut[i] = src[clutTableT16I4[i]];
|
|
}
|
|
}
|
|
|
|
// These functions are only used if the CLUT is in 32bit mode and it swaps to CSM2, a very obscure setup used by Breath of Fire Dragon Quarter.
|
|
// This doesn't handle offsetting the CLUT or anything crazy, that would be a lot more work
|
|
void GSClut::ReadCLUT_T32_I4_Swizzled(const u16* RESTRICT clut, u32* RESTRICT dst)
|
|
{
|
|
// Point to the base of the palette block
|
|
GSVector4i* s = (GSVector4i*)clut;
|
|
GSVector4i* d = (GSVector4i*)dst;
|
|
|
|
GSVector4i v0 = s[0];
|
|
GSVector4i v1 = s[2];
|
|
GSVector4i v2 = s[32];
|
|
GSVector4i v3 = s[34];
|
|
|
|
GSVector4i::sw16(v0, v2, v1, v3);
|
|
|
|
d[0] = v0;
|
|
d[1] = v1;
|
|
d[2] = v2;
|
|
d[3] = v3;
|
|
}
|
|
|
|
void GSClut::ReadCLUT_T32_I8(const u16* RESTRICT clut, u32* RESTRICT dst, int offset)
|
|
{
|
|
// Okay this deserves a small explanation
|
|
// T32 I8 can address up to 256 colors however the offset can be "more than zero" when reading
|
|
// Previously I assumed that it would wrap around the end of the buffer to the beginning
|
|
// but it turns out this is incorrect, the address doesn't mirror, it clamps to to the last offset,
|
|
// probably though some sort of addressing mechanism then picks the color from the lower 0xF of the requested CLUT entry.
|
|
// if we don't do this, the dirt on GTA SA goes transparent and actually cleans the car driving through dirt.
|
|
for (int i = 0; i < 256; i += 16)
|
|
{
|
|
// Min value + offet or Last CSA * 16 (240)
|
|
ReadCLUT_T32_I4(&clut[std::min((i + offset), 240)], &dst[i]);
|
|
}
|
|
}
|
|
|
|
__forceinline void GSClut::ReadCLUT_T32_I4(const u16* RESTRICT clut, u32* RESTRICT dst)
|
|
{
|
|
GSVector4i* s = (GSVector4i*)clut;
|
|
GSVector4i* d = (GSVector4i*)dst;
|
|
|
|
GSVector4i v0 = s[0];
|
|
GSVector4i v1 = s[1];
|
|
GSVector4i v2 = s[32];
|
|
GSVector4i v3 = s[33];
|
|
|
|
GSVector4i::sw16(v0, v2, v1, v3);
|
|
|
|
d[0] = v0;
|
|
d[1] = v1;
|
|
d[2] = v2;
|
|
d[3] = v3;
|
|
}
|
|
|
|
#if 0
|
|
__forceinline void GSClut::ReadCLUT_T32_I4(const u16* RESTRICT clut, u32* RESTRICT dst32, u64* RESTRICT dst64)
|
|
{
|
|
GSVector4i* s = (GSVector4i*)clut;
|
|
GSVector4i* d32 = (GSVector4i*)dst32;
|
|
GSVector4i* d64 = (GSVector4i*)dst64;
|
|
|
|
GSVector4i s0 = s[0];
|
|
GSVector4i s1 = s[1];
|
|
GSVector4i s2 = s[32];
|
|
GSVector4i s3 = s[33];
|
|
|
|
GSVector4i::sw16(s0, s2, s1, s3);
|
|
|
|
d32[0] = s0;
|
|
d32[1] = s1;
|
|
d32[2] = s2;
|
|
d32[3] = s3;
|
|
|
|
ExpandCLUT64_T32(s0, s0, s1, s2, s3, &d64[0]);
|
|
ExpandCLUT64_T32(s1, s0, s1, s2, s3, &d64[32]);
|
|
ExpandCLUT64_T32(s2, s0, s1, s2, s3, &d64[64]);
|
|
ExpandCLUT64_T32(s3, s0, s1, s2, s3, &d64[96]);
|
|
}
|
|
#endif
|
|
|
|
#if 0
|
|
void GSClut::ReadCLUT_T16_I8(const u16* RESTRICT clut, u32* RESTRICT dst)
|
|
{
|
|
for(int i = 0; i < 256; i += 16)
|
|
{
|
|
ReadCLUT_T16_I4(&clut[i], &dst[i]);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if 0
|
|
__forceinline void GSClut::ReadCLUT_T16_I4(const u16* RESTRICT clut, u32* RESTRICT dst)
|
|
{
|
|
GSVector4i* s = (GSVector4i*)clut;
|
|
GSVector4i* d = (GSVector4i*)dst;
|
|
|
|
GSVector4i v0 = s[0];
|
|
GSVector4i v1 = s[1];
|
|
|
|
d[0] = v0.upl16();
|
|
d[1] = v0.uph16();
|
|
d[2] = v1.upl16();
|
|
d[3] = v1.uph16();
|
|
}
|
|
#endif
|
|
|
|
#if 0
|
|
__forceinline void GSClut::ReadCLUT_T16_I4(const u16* RESTRICT clut, u32* RESTRICT dst32, u64* RESTRICT dst64)
|
|
{
|
|
GSVector4i* s = (GSVector4i*)clut;
|
|
GSVector4i* d32 = (GSVector4i*)dst32;
|
|
GSVector4i* d64 = (GSVector4i*)dst64;
|
|
|
|
GSVector4i v0 = s[0];
|
|
GSVector4i v1 = s[1];
|
|
|
|
GSVector4i s0 = v0.upl16();
|
|
GSVector4i s1 = v0.uph16();
|
|
GSVector4i s2 = v1.upl16();
|
|
GSVector4i s3 = v1.uph16();
|
|
|
|
d32[0] = s0;
|
|
d32[1] = s1;
|
|
d32[2] = s2;
|
|
d32[3] = s3;
|
|
|
|
ExpandCLUT64_T16(s0, s0, s1, s2, s3, &d64[0]);
|
|
ExpandCLUT64_T16(s1, s0, s1, s2, s3, &d64[32]);
|
|
ExpandCLUT64_T16(s2, s0, s1, s2, s3, &d64[64]);
|
|
ExpandCLUT64_T16(s3, s0, s1, s2, s3, &d64[96]);
|
|
}
|
|
#endif
|
|
|
|
void GSClut::ExpandCLUT64_T32_I8(const u32* RESTRICT src, u64* RESTRICT dst)
|
|
{
|
|
GSVector4i* s = (GSVector4i*)src;
|
|
GSVector4i* d = (GSVector4i*)dst;
|
|
|
|
const GSVector4i s0 = s[0];
|
|
const GSVector4i s1 = s[1];
|
|
const GSVector4i s2 = s[2];
|
|
const GSVector4i s3 = s[3];
|
|
|
|
ExpandCLUT64_T32(s0, s0, s1, s2, s3, &d[0]);
|
|
ExpandCLUT64_T32(s1, s0, s1, s2, s3, &d[32]);
|
|
ExpandCLUT64_T32(s2, s0, s1, s2, s3, &d[64]);
|
|
ExpandCLUT64_T32(s3, s0, s1, s2, s3, &d[96]);
|
|
}
|
|
|
|
__forceinline void GSClut::ExpandCLUT64_T32(const GSVector4i& hi, const GSVector4i& lo0, const GSVector4i& lo1, const GSVector4i& lo2, const GSVector4i& lo3, GSVector4i* dst)
|
|
{
|
|
ExpandCLUT64_T32(hi.xxxx(), lo0, &dst[0]);
|
|
ExpandCLUT64_T32(hi.xxxx(), lo1, &dst[2]);
|
|
ExpandCLUT64_T32(hi.xxxx(), lo2, &dst[4]);
|
|
ExpandCLUT64_T32(hi.xxxx(), lo3, &dst[6]);
|
|
ExpandCLUT64_T32(hi.yyyy(), lo0, &dst[8]);
|
|
ExpandCLUT64_T32(hi.yyyy(), lo1, &dst[10]);
|
|
ExpandCLUT64_T32(hi.yyyy(), lo2, &dst[12]);
|
|
ExpandCLUT64_T32(hi.yyyy(), lo3, &dst[14]);
|
|
ExpandCLUT64_T32(hi.zzzz(), lo0, &dst[16]);
|
|
ExpandCLUT64_T32(hi.zzzz(), lo1, &dst[18]);
|
|
ExpandCLUT64_T32(hi.zzzz(), lo2, &dst[20]);
|
|
ExpandCLUT64_T32(hi.zzzz(), lo3, &dst[22]);
|
|
ExpandCLUT64_T32(hi.wwww(), lo0, &dst[24]);
|
|
ExpandCLUT64_T32(hi.wwww(), lo1, &dst[26]);
|
|
ExpandCLUT64_T32(hi.wwww(), lo2, &dst[28]);
|
|
ExpandCLUT64_T32(hi.wwww(), lo3, &dst[30]);
|
|
}
|
|
|
|
__forceinline void GSClut::ExpandCLUT64_T32(const GSVector4i& hi, const GSVector4i& lo, GSVector4i* dst)
|
|
{
|
|
dst[0] = lo.upl32(hi);
|
|
dst[1] = lo.uph32(hi);
|
|
}
|
|
|
|
#if 0
|
|
void GSClut::ExpandCLUT64_T16_I8(const u32* RESTRICT src, u64* RESTRICT dst)
|
|
{
|
|
GSVector4i* s = (GSVector4i*)src;
|
|
GSVector4i* d = (GSVector4i*)dst;
|
|
|
|
GSVector4i s0 = s[0];
|
|
GSVector4i s1 = s[1];
|
|
GSVector4i s2 = s[2];
|
|
GSVector4i s3 = s[3];
|
|
|
|
ExpandCLUT64_T16(s0, s0, s1, s2, s3, &d[0]);
|
|
ExpandCLUT64_T16(s1, s0, s1, s2, s3, &d[32]);
|
|
ExpandCLUT64_T16(s2, s0, s1, s2, s3, &d[64]);
|
|
ExpandCLUT64_T16(s3, s0, s1, s2, s3, &d[96]);
|
|
}
|
|
#endif
|
|
|
|
__forceinline void GSClut::ExpandCLUT64_T16(const GSVector4i& hi, const GSVector4i& lo0, const GSVector4i& lo1, const GSVector4i& lo2, const GSVector4i& lo3, GSVector4i* dst)
|
|
{
|
|
ExpandCLUT64_T16(hi.xxxx(), lo0, &dst[0]);
|
|
ExpandCLUT64_T16(hi.xxxx(), lo1, &dst[2]);
|
|
ExpandCLUT64_T16(hi.xxxx(), lo2, &dst[4]);
|
|
ExpandCLUT64_T16(hi.xxxx(), lo3, &dst[6]);
|
|
ExpandCLUT64_T16(hi.yyyy(), lo0, &dst[8]);
|
|
ExpandCLUT64_T16(hi.yyyy(), lo1, &dst[10]);
|
|
ExpandCLUT64_T16(hi.yyyy(), lo2, &dst[12]);
|
|
ExpandCLUT64_T16(hi.yyyy(), lo3, &dst[14]);
|
|
ExpandCLUT64_T16(hi.zzzz(), lo0, &dst[16]);
|
|
ExpandCLUT64_T16(hi.zzzz(), lo1, &dst[18]);
|
|
ExpandCLUT64_T16(hi.zzzz(), lo2, &dst[20]);
|
|
ExpandCLUT64_T16(hi.zzzz(), lo3, &dst[22]);
|
|
ExpandCLUT64_T16(hi.wwww(), lo0, &dst[24]);
|
|
ExpandCLUT64_T16(hi.wwww(), lo1, &dst[26]);
|
|
ExpandCLUT64_T16(hi.wwww(), lo2, &dst[28]);
|
|
ExpandCLUT64_T16(hi.wwww(), lo3, &dst[30]);
|
|
}
|
|
|
|
__forceinline void GSClut::ExpandCLUT64_T16(const GSVector4i& hi, const GSVector4i& lo, GSVector4i* dst)
|
|
{
|
|
dst[0] = lo.upl16(hi);
|
|
dst[1] = lo.uph16(hi);
|
|
}
|
|
|
|
// TODO
|
|
|
|
constinit const GSVector4i GSClut::m_bm = GSVector4i::cxpr(0x00007c00);
|
|
constinit const GSVector4i GSClut::m_gm = GSVector4i::cxpr(0x000003e0);
|
|
constinit const GSVector4i GSClut::m_rm = GSVector4i::cxpr(0x0000001f);
|
|
|
|
void GSClut::Expand16(const u16* RESTRICT src, u32* RESTRICT dst, int w, const GIFRegTEXA& TEXA)
|
|
{
|
|
pxAssert((w & 7) == 0);
|
|
|
|
const GSVector4i rm = m_rm;
|
|
const GSVector4i gm = m_gm;
|
|
const GSVector4i bm = m_bm;
|
|
|
|
const GSVector4i TA0(TEXA.TA0 << 24);
|
|
const GSVector4i TA1(TEXA.TA1 << 24);
|
|
|
|
GSVector4i c, cl, ch;
|
|
|
|
const GSVector4i* s = (const GSVector4i*)src;
|
|
GSVector4i* d = (GSVector4i*)dst;
|
|
|
|
if (!TEXA.AEM)
|
|
{
|
|
for (int i = 0, j = w >> 3; i < j; i++)
|
|
{
|
|
c = s[i];
|
|
cl = c.upl16(c);
|
|
ch = c.uph16(c);
|
|
d[i * 2 + 0] = ((cl & rm) << 3) | ((cl & gm) << 6) | ((cl & bm) << 9) | TA0.blend8(TA1, cl.sra16<15>());
|
|
d[i * 2 + 1] = ((ch & rm) << 3) | ((ch & gm) << 6) | ((ch & bm) << 9) | TA0.blend8(TA1, ch.sra16<15>());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0, j = w >> 3; i < j; i++)
|
|
{
|
|
c = s[i];
|
|
cl = c.upl16(c);
|
|
ch = c.uph16(c);
|
|
d[i * 2 + 0] = ((cl & rm) << 3) | ((cl & gm) << 6) | ((cl & bm) << 9) | TA0.blend8(TA1, cl.sra16<15>()).andnot(cl == GSVector4i::zero());
|
|
d[i * 2 + 1] = ((ch & rm) << 3) | ((ch & gm) << 6) | ((ch & bm) << 9) | TA0.blend8(TA1, ch.sra16<15>()).andnot(ch == GSVector4i::zero());
|
|
}
|
|
}
|
|
}
|
|
|
|
bool GSClut::WriteState::IsDirty(const GIFRegTEX0& TEX0, const GIFRegTEXCLUT& TEXCLUT)
|
|
{
|
|
constexpr u64 mask = 0x1FFFFFE000000000ull; // CSA CSM CPSM CBP
|
|
|
|
bool is_dirty = dirty;
|
|
|
|
if (((this->TEX0.U64 ^ TEX0.U64) & mask) || (GSLocalMemory::m_psm[this->TEX0.PSM].pal != GSLocalMemory::m_psm[TEX0.PSM].pal))
|
|
is_dirty |= true;
|
|
else if (TEX0.CSM == 1 && (TEXCLUT.U32[0] ^ this->TEXCLUT.U32[0]))
|
|
is_dirty |= true;
|
|
|
|
if (!is_dirty)
|
|
{
|
|
this->TEX0.U64 = TEX0.U64;
|
|
this->TEXCLUT.U64 = TEXCLUT.U64;
|
|
}
|
|
|
|
return is_dirty;
|
|
}
|
|
|
|
bool GSClut::ReadState::IsDirty(const GIFRegTEX0& TEX0)
|
|
{
|
|
constexpr u64 mask = 0x1FFFFFE000000000ull; // CSA CSM CPSM CBP
|
|
|
|
bool is_dirty = dirty;
|
|
|
|
if (((this->TEX0.U64 ^ TEX0.U64) & mask) || (GSLocalMemory::m_psm[this->TEX0.PSM].pal != GSLocalMemory::m_psm[TEX0.PSM].pal))
|
|
is_dirty |= true;
|
|
|
|
if (!is_dirty)
|
|
{
|
|
this->TEX0.U64 = TEX0.U64;
|
|
}
|
|
|
|
return is_dirty;
|
|
}
|
|
|
|
bool GSClut::ReadState::IsDirty(const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA)
|
|
{
|
|
constexpr u64 tex0_mask = 0x1FFFFFE000000000ull; // CSA CSM CPSM CBP
|
|
constexpr u64 texa24_mask = 0x80FFull; // AEM TA0
|
|
constexpr u64 texa16_mask = 0xFF000080FFull; // TA1 AEM TA0
|
|
|
|
bool is_dirty = dirty;
|
|
|
|
if (((this->TEX0.U64 ^ TEX0.U64) & tex0_mask) || (GSLocalMemory::m_psm[this->TEX0.PSM].pal != GSLocalMemory::m_psm[TEX0.PSM].pal))
|
|
is_dirty |= true;
|
|
else // Just to optimise the checks.
|
|
{
|
|
// Check TA0 and AEM in 24bit mode.
|
|
if (TEX0.CPSM == PSMCT24 && ((this->TEXA.U64 ^ TEXA.U64) & texa24_mask))
|
|
is_dirty |= true;
|
|
// Check all fields in 16bit mode.
|
|
else if (TEX0.CPSM >= PSMCT16 && ((this->TEXA.U64 ^ TEXA.U64) & texa16_mask))
|
|
is_dirty |= true;
|
|
}
|
|
|
|
if (!is_dirty)
|
|
{
|
|
this->TEX0.U64 = TEX0.U64;
|
|
this->TEXA.U64 = TEXA.U64;
|
|
}
|
|
|
|
return is_dirty;
|
|
}
|