Merge pull request #22329 from hrydgard/vertex-decoder-arm64-morph

Vertex decoder: JIT morph on arm64
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-09-21 17:56:15 -06:00
commit ab6ccbd08c
2 files changed
+231 -3

No files matched your search

+227 -3
View File
@@ -123,7 +123,13 @@ static const JitLookup jitLookup[] = {
{&VertexDecoder::Step_PosS16Skin, &VertexDecoderJitCache::Jit_PosS16Skin},
{&VertexDecoder::Step_PosFloatSkin, &VertexDecoderJitCache::Jit_PosFloatSkin},
/*
{&VertexDecoder::Step_TcU8MorphToFloat, &VertexDecoderJitCache::Jit_TcU8MorphToFloat},
{&VertexDecoder::Step_TcU16MorphToFloat, &VertexDecoderJitCache::Jit_TcU16MorphToFloat},
{&VertexDecoder::Step_TcFloatMorph, &VertexDecoderJitCache::Jit_TcFloatMorph},
{&VertexDecoder::Step_TcU8PrescaleMorph, &VertexDecoderJitCache::Jit_TcU8PrescaleMorph},
{&VertexDecoder::Step_TcU16PrescaleMorph, &VertexDecoderJitCache::Jit_TcU16PrescaleMorph},
{&VertexDecoder::Step_TcFloatPrescaleMorph, &VertexDecoderJitCache::Jit_TcFloatPrescaleMorph},
{&VertexDecoder::Step_NormalS8Morph, &VertexDecoderJitCache::Jit_NormalS8Morph},
{&VertexDecoder::Step_NormalS16Morph, &VertexDecoderJitCache::Jit_NormalS16Morph},
{&VertexDecoder::Step_NormalFloatMorph, &VertexDecoderJitCache::Jit_NormalFloatMorph},
@@ -136,7 +142,6 @@ static const JitLookup jitLookup[] = {
{&VertexDecoder::Step_Color4444Morph, &VertexDecoderJitCache::Jit_Color4444Morph},
{&VertexDecoder::Step_Color565Morph, &VertexDecoderJitCache::Jit_Color565Morph},
{&VertexDecoder::Step_Color5551Morph, &VertexDecoderJitCache::Jit_Color5551Morph},
*/
};
@@ -156,7 +161,10 @@ JittedVertexDecoder VertexDecoderJitCache::Compile(const VertexDecoder &dec, int
for (int i = 0; i < dec.numSteps_; i++) {
if (dec.steps_[i] == &VertexDecoder::Step_TcU8Prescale ||
dec.steps_[i] == &VertexDecoder::Step_TcU16Prescale ||
dec.steps_[i] == &VertexDecoder::Step_TcFloatPrescale) {
dec.steps_[i] == &VertexDecoder::Step_TcFloatPrescale ||
dec.steps_[i] == &VertexDecoder::Step_TcU8PrescaleMorph ||
dec.steps_[i] == &VertexDecoder::Step_TcU16PrescaleMorph ||
dec.steps_[i] == &VertexDecoder::Step_TcFloatPrescaleMorph) {
prescaleStep = true;
}
if (dec.steps_[i] == &VertexDecoder::Step_WeightsU8Skin ||
@@ -755,4 +763,220 @@ void VertexDecoderJitCache::Jit_WriteMatrixMul(int outOff, bool pos) {
fp.STUR(128, accNEON, dstReg, outOff);
}
// Morph. Each step sums its attribute over the morph frames exactly the way its step function
// does, so the results match bit for bit. Where that step is plain C++, the compiler fuses its
// multiply-adds on arm64, so we use FMLA; where it's CrossSIMD (separate multiply and add
// intrinsics), we don't.
//
// Registers: Q6 is the sum, Q2 the frame's data, Q3 its weight, and Q4, Q5 constants. That's
// part of the skin matrix (Q4-Q7), which is fine: the *MorphSkin steps aren't JITed, so no decoder
// with a morph step here ever uses that matrix. Q8-Q15 would need saving, Q16+ hold bones.
static const ARM64Reg morphSumQ = Q6;
static const ARM64Reg morphSumD = D6;
// Walks the morph frames, since their offsets can be past what LDUR reaches.
static const ARM64Reg morphFrameReg = X7;
void VertexDecoderJitCache::Jit_MorphSum(MorphInput input, int srcoff, int fracBits, bool fused) {
MOVP2R(tempRegPtr, &gstate_c.morphWeights[0]);
ADDI2R(morphFrameReg, srcReg, srcoff, scratchReg);
fp.MOVI(32, morphSumQ, 0);
for (int n = 0; n < dec_->morphcount; n++) {
if (n > 0) {
ADDI2R(morphFrameReg, morphFrameReg, dec_->onesize_, scratchReg);
}
// Scaling by a power of two commutes with rounding, so doing it on load (the fixed point
// conversions) instead of after the sum like some of the steps gives the same result.
switch (input) {
case MorphInput::S8x3:
fp.LDUR(32, S2, morphFrameReg, 0);
fp.SXTL(8, D2, S2);
fp.SXTL(16, Q2, D2);
break;
case MorphInput::S16x3:
fp.LDUR(64, D2, morphFrameReg, 0);
fp.SXTL(16, Q2, D2);
break;
case MorphInput::U8x2:
fp.LDUR(16, D2, morphFrameReg, 0);
fp.UXTL(8, Q2, D2);
fp.UXTL(16, Q2, D2);
break;
case MorphInput::U16x2:
fp.LDUR(32, D2, morphFrameReg, 0);
fp.UXTL(16, Q2, D2);
break;
case MorphInput::U8x4:
fp.LDUR(32, S2, morphFrameReg, 0);
fp.UXTL(8, Q2, D2);
fp.UXTL(16, Q2, D2);
break;
case MorphInput::F32x2:
fp.LDUR(64, D2, morphFrameReg, 0);
break;
case MorphInput::F32x3:
fp.LDUR(128, Q2, morphFrameReg, 0);
break;
}
if (input == MorphInput::S8x3 || input == MorphInput::S16x3) {
if (fracBits)
fp.SCVTF(32, Q2, Q2, fracBits);
else
fp.SCVTF(32, Q2, Q2);
} else if (input != MorphInput::F32x2 && input != MorphInput::F32x3) {
if (fracBits)
fp.UCVTF(32, Q2, Q2, fracBits);
else
fp.UCVTF(32, Q2, Q2);
}
fp.LDR(32, INDEX_UNSIGNED, S3, tempRegPtr, n * 4);
if (fused) {
fp.FMLA(32, morphSumQ, Q2, Q3, 0);
} else {
fp.FMUL(32, Q2, Q2, Q3, 0);
fp.FADD(32, morphSumQ, morphSumQ, Q2);
}
}
}
void VertexDecoderJitCache::Jit_TcU8MorphToFloat() {
Jit_MorphSum(MorphInput::U8x2, dec_->tcoff, 7, true);
fp.STUR(64, morphSumD, dstReg, dec_->decFmt.uvoff);
}
void VertexDecoderJitCache::Jit_TcU16MorphToFloat() {
Jit_MorphSum(MorphInput::U16x2, dec_->tcoff, 15, true);
fp.STUR(64, morphSumD, dstReg, dec_->decFmt.uvoff);
}
void VertexDecoderJitCache::Jit_TcFloatMorph() {
Jit_MorphSum(MorphInput::F32x2, dec_->tcoff, 0, true);
fp.STUR(64, morphSumD, dstReg, dec_->decFmt.uvoff);
}
// The steps round sum * scale before applying the 1/128 (or 1/32768) and adding the offset. With the
// fraction already applied to the sum, multiplying by the scale gives that same rounding, and the
// offset is then added separately.
void VertexDecoderJitCache::Jit_TcU8PrescaleMorph() {
Jit_MorphSum(MorphInput::U8x2, dec_->tcoff, 7, true);
fp.FMUL(32, morphSumD, morphSumD, neonUVScaleReg);
fp.FADD(32, morphSumD, morphSumD, neonUVOffsetReg);
fp.STUR(64, morphSumD, dstReg, dec_->decFmt.uvoff);
}
void VertexDecoderJitCache::Jit_TcU16PrescaleMorph() {
Jit_MorphSum(MorphInput::U16x2, dec_->tcoff, 15, true);
fp.FMUL(32, morphSumD, morphSumD, neonUVScaleReg);
fp.FADD(32, morphSumD, morphSumD, neonUVOffsetReg);
fp.STUR(64, morphSumD, dstReg, dec_->decFmt.uvoff);
}
void VertexDecoderJitCache::Jit_TcFloatPrescaleMorph() {
Jit_MorphSum(MorphInput::F32x2, dec_->tcoff, 0, true);
fp.MOV(neonScratchRegD, neonUVOffsetReg);
fp.FMLA(32, neonScratchRegD, morphSumD, neonUVScaleReg);
fp.STUR(64, neonScratchRegD, dstReg, dec_->decFmt.uvoff);
}
void VertexDecoderJitCache::Jit_NormalS8Morph() {
Jit_MorphSum(MorphInput::S8x3, dec_->nrmoff, 7, false);
fp.STUR(128, morphSumQ, dstReg, dec_->decFmt.nrmoff);
}
void VertexDecoderJitCache::Jit_NormalS16Morph() {
Jit_MorphSum(MorphInput::S16x3, dec_->nrmoff, 15, true);
fp.STUR(128, morphSumQ, dstReg, dec_->decFmt.nrmoff);
}
void VertexDecoderJitCache::Jit_NormalFloatMorph() {
Jit_MorphSum(MorphInput::F32x3, dec_->nrmoff, 0, false);
fp.STUR(128, morphSumQ, dstReg, dec_->decFmt.nrmoff);
}
void VertexDecoderJitCache::Jit_PosS8Morph() {
Jit_MorphSum(MorphInput::S8x3, dec_->posoff, 7, true);
fp.STUR(128, morphSumQ, dstReg, dec_->decFmt.posoff);
}
void VertexDecoderJitCache::Jit_PosS16Morph() {
Jit_MorphSum(MorphInput::S16x3, dec_->posoff, 15, false);
fp.STUR(128, morphSumQ, dstReg, dec_->decFmt.posoff);
}
void VertexDecoderJitCache::Jit_PosFloatMorph() {
Jit_MorphSum(MorphInput::F32x3, dec_->posoff, 0, true);
fp.STUR(128, morphSumQ, dstReg, dec_->decFmt.posoff);
}
// Truncates the RGBA sum to bytes and stores it, like clamp_u8((int)x) in the steps.
void VertexDecoderJitCache::Jit_WriteMorphColorArm64(bool checkAlpha, bool forceFullAlpha) {
fp.FCVTZU(32, morphSumQ, morphSumQ);
fp.UQXTN(16, morphSumD, morphSumQ);
fp.UQXTN(8, morphSumD, morphSumQ);
fp.UMOV(32, tempReg1, morphSumQ, 0);
if (forceFullAlpha) {
ORRI2R(tempReg1, tempReg1, 0xFF000000, scratchReg);
} else if (checkAlpha) {
// Or any non-set bits into alphaNonFullReg. This way it's non-zero if not full.
ORN(alphaNonFullReg, alphaNonFullReg, tempReg1, ArithOption(tempReg1, ST_ASR, 24));
}
STR(INDEX_UNSIGNED, tempReg1, dstReg, dec_->decFmt.c0off);
}
void VertexDecoderJitCache::Jit_Color8888Morph() {
Jit_MorphSum(MorphInput::U8x4, dec_->coloff, 0, false);
Jit_WriteMorphColorArm64(true, false);
}
// The steps compute w * c * k per channel and add that to the sum, which the compiler fuses. w * c
// is exact, so a multiply followed by a fused multiply-add gives the same result. Instead of shifting
// each channel down, the masks leave it in place, and k is divided by the same power of two, which
// keeps w * c exact and the product with k unchanged.
struct alignas(16) ColorMorphConstants {
u32 masks[4];
float scales[4];
};
static const ColorMorphConstants color565Morph = {
{ 0x001F, 0x07E0, 0xF800, 0 },
{ 255.0f / 31.0f, (255.0f / 63.0f) / 32.0f, (255.0f / 31.0f) / 2048.0f, 0.0f },
};
static const ColorMorphConstants color5551Morph = {
{ 0x001F, 0x03E0, 0x7C00, 0x8000 },
{ 255.0f / 31.0f, (255.0f / 31.0f) / 32.0f, (255.0f / 31.0f) / 1024.0f, 255.0f / 32768.0f },
};
static const ColorMorphConstants color4444Morph = {
{ 0x000F, 0x00F0, 0x0F00, 0xF000 },
{ 255.0f / 15.0f, (255.0f / 15.0f) / 16.0f, (255.0f / 15.0f) / 256.0f, (255.0f / 15.0f) / 4096.0f },
};
void VertexDecoderJitCache::Jit_ColorMorph16(const void *constants, bool fullAlpha) {
MOVP2R(scratchReg64, constants);
fp.LDR(128, INDEX_UNSIGNED, Q4, scratchReg64, offsetof(ColorMorphConstants, masks));
fp.LDR(128, INDEX_UNSIGNED, Q5, scratchReg64, offsetof(ColorMorphConstants, scales));
MOVP2R(tempRegPtr, &gstate_c.morphWeights[0]);
fp.MOVI(32, morphSumQ, 0);
for (int n = 0; n < dec_->morphcount; n++) {
LDRH(INDEX_UNSIGNED, tempReg2, srcReg, dec_->onesize_ * n + dec_->coloff);
fp.DUP(32, Q2, tempReg2);
fp.AND(Q2, Q2, Q4);
fp.UCVTF(32, Q2, Q2);
fp.LDR(32, INDEX_UNSIGNED, S3, tempRegPtr, n * 4);
fp.FMUL(32, Q2, Q2, Q3, 0);
fp.FMLA(32, morphSumQ, Q2, Q5);
}
Jit_WriteMorphColorArm64(!fullAlpha, fullAlpha);
}
void VertexDecoderJitCache::Jit_Color565Morph() {
Jit_ColorMorph16(&color565Morph, true);
}
void VertexDecoderJitCache::Jit_Color5551Morph() {
Jit_ColorMorph16(&color5551Morph, false);
}
void VertexDecoderJitCache::Jit_Color4444Morph() {
Jit_ColorMorph16(&color4444Morph, false);
}
#endif // PPSSPP_ARCH(ARM64)
+4
View File
@@ -421,6 +421,10 @@ private:
const VertexDecoder *dec_ = nullptr;
#if PPSSPP_ARCH(ARM64)
enum class MorphInput { S8x3, S16x3, U8x2, U16x2, U8x4, F32x2, F32x3 };
void Jit_MorphSum(MorphInput input, int srcoff, int fracBits, bool fused);
void Jit_ColorMorph16(const void *constants, bool fullAlpha);
void Jit_WriteMorphColorArm64(bool checkAlpha, bool forceFullAlpha);
Arm64Gen::ARM64FloatEmitter fp;
#endif
};