Files
ARMSX2/pcsx2/GS/GSClut.cpp
Brian Degenhardt 3e56da7f86 Merge upstream PCSX2 (2026-07-15 .. 2026-08-10)
71 commits from 474ad59818 to 2cf8dabe6b, 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 by
  431ca0c063, 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.
2026-08-10 18:24:24 -07:00

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;
}