// Copyright (c) 2017- PPSSPP Project. // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, version 2.0 or later versions. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License 2.0 for more details. // A copy of the GPL 2.0 should have been included with the program. // If not, see http://www.gnu.org/licenses/ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include "ppsspp_config.h" #include #include "Common/Common.h" #include "Common/Data/Convert/ColorConv.h" #include "Common/LogReporting.h" #include "Common/Math/CrossSIMD.h" #include "Common/Math/SIMDHeaders.h" #include "Core/Config.h" #include "GPU/Common/TextureDecoder.h" #include "GPU/Software/BinManager.h" #include "GPU/Software/Rasterizer.h" #include "GPU/Software/RasterizerTypes.h" #include "GPU/Software/Sampler.h" using namespace Math3D; using namespace Rasterizer; namespace Sampler { static Vec4IntResult SOFTRAST_CALL SampleNearest(float s, float t, Vec4IntArg prim_color, const u8 *const *tptr, const uint16_t *bufw, int level, int levelFrac, const SamplerID &samplerID); static Vec4IntResult SOFTRAST_CALL SampleLinear(float s, float t, Vec4IntArg prim_color, const u8 *const *tptr, const uint16_t *bufw, int level, int levelFrac, const SamplerID &samplerID); static Vec4IntResult SOFTRAST_CALL SampleFetch(int u, int v, const u8 *tptr, int bufw, int level, const SamplerID &samplerID); static LinearFunc GetLinearFallback(const SamplerID &id); NearestFunc GetNearestFunc(const SamplerID &id) { return &SampleNearest; } LinearFunc GetLinearFunc(const SamplerID &id) { return GetLinearFallback(id); } FetchFunc GetFetchFunc(const SamplerID &id) { return &SampleFetch; } template static inline int GetPixelDataOffset(uint32_t row_pitch_pixels, uint32_t u, uint32_t v, bool swizzled) { if (!swizzled) return (v * (row_pitch_pixels * texel_size_bits >> 3)) + (u * texel_size_bits >> 3); constexpr uint32_t tile_size_bits = 32; constexpr uint32_t tiles_in_block_horizontal = 4; constexpr uint32_t tiles_in_block_vertical = 8; constexpr uint32_t texels_per_tile = tile_size_bits / texel_size_bits; uint32_t tile_u = u / texels_per_tile; uint32_t tile_idx = (v % tiles_in_block_vertical) * (tiles_in_block_horizontal) + // TODO: not sure if the *texel_size_bits/8 factor is correct (v / tiles_in_block_vertical) * ((row_pitch_pixels*texel_size_bits/(tile_size_bits))*tiles_in_block_vertical) + (tile_u % tiles_in_block_horizontal) + (tile_u / tiles_in_block_horizontal) * (tiles_in_block_horizontal*tiles_in_block_vertical); return tile_idx * (tile_size_bits / 8) + ((u % texels_per_tile) * texel_size_bits) / 8; } static inline u32 LookupColor(unsigned int index, unsigned int level, const SamplerID &samplerID) { int clutSharingOffset = 0; if (!samplerID.useSharedClut) clutSharingOffset = samplerID.TexFmt() == GE_TFMT_CLUT4 ? level * 16 : (level & 1) * 256; switch (samplerID.ClutFmt()) { case GE_CMODE_16BIT_BGR5650: return RGB565ToRGBA8888(samplerID.cached.clut16[index + clutSharingOffset]); case GE_CMODE_16BIT_ABGR5551: return RGBA5551ToRGBA8888(samplerID.cached.clut16[index + clutSharingOffset]); case GE_CMODE_16BIT_ABGR4444: return RGBA4444ToRGBA8888(samplerID.cached.clut16[index + clutSharingOffset]); case GE_CMODE_32BIT_ABGR8888: return samplerID.cached.clut32[index + clutSharingOffset]; default: ERROR_LOG_REPORT(Log::G3D, "Software: Unsupported palette format: %x", samplerID.ClutFmt()); return 0; } } static inline uint32_t TransformClutIndex(uint32_t index, const SamplerID &samplerID) { if (samplerID.hasClutShift || samplerID.hasClutMask || samplerID.hasClutOffset) { const uint8_t shift = (samplerID.cached.clutFormat >> 2) & 0x1F; const uint8_t mask = (samplerID.cached.clutFormat >> 8) & 0xFF; const uint16_t offset = ((samplerID.cached.clutFormat >> 16) & 0x1F) << 4; // We need to wrap any entries beyond the first 1024 bytes. const uint16_t offsetMask = samplerID.ClutFmt() == GE_CMODE_32BIT_ABGR8888 ? 0xFF : 0x1FF; return ((index >> shift) & mask) | (offset & offsetMask); } return index & 0xFF; } struct Nearest4 { alignas(16) u32 v[4]; operator u32() const { return v[0]; } }; template inline static Nearest4 SOFTRAST_CALL SampleNearest(const int u[N], const int v[N], const u8 *srcptr, uint16_t texbufw, int level, const SamplerID &samplerID) { Nearest4 res; if (!srcptr) { memset(res.v, 0, sizeof(res.v)); return res; } // TODO: Should probably check if textures are aligned properly... switch (samplerID.TexFmt()) { case GE_TFMT_4444: for (int i = 0; i < N; ++i) { const u8 *src = srcptr + GetPixelDataOffset<16>(texbufw, u[i], v[i], samplerID.swizzle); res.v[i] = RGBA4444ToRGBA8888(*(const u16 *)src); } return res; case GE_TFMT_5551: for (int i = 0; i < N; ++i) { const u8 *src = srcptr + GetPixelDataOffset<16>(texbufw, u[i], v[i], samplerID.swizzle); res.v[i] = RGBA5551ToRGBA8888(*(const u16 *)src); } return res; case GE_TFMT_5650: for (int i = 0; i < N; ++i) { const u8 *src = srcptr + GetPixelDataOffset<16>(texbufw, u[i], v[i], samplerID.swizzle); res.v[i] = RGB565ToRGBA8888(*(const u16 *)src); } return res; case GE_TFMT_8888: for (int i = 0; i < N; ++i) { const u8 *src = srcptr + GetPixelDataOffset<32>(texbufw, u[i], v[i], samplerID.swizzle); res.v[i] = *(const u32 *)src; } return res; case GE_TFMT_CLUT32: for (int i = 0; i < N; ++i) { const u8 *src = srcptr + GetPixelDataOffset<32>(texbufw, u[i], v[i], samplerID.swizzle); u32 val = src[0] + (src[1] << 8) + (src[2] << 16) + (src[3] << 24); res.v[i] = LookupColor(TransformClutIndex(val, samplerID), 0, samplerID); } return res; case GE_TFMT_CLUT16: for (int i = 0; i < N; ++i) { const u8 *src = srcptr + GetPixelDataOffset<16>(texbufw, u[i], v[i], samplerID.swizzle); u16 val = src[0] + (src[1] << 8); res.v[i] = LookupColor(TransformClutIndex(val, samplerID), 0, samplerID); } return res; case GE_TFMT_CLUT8: for (int i = 0; i < N; ++i) { const u8 *src = srcptr + GetPixelDataOffset<8>(texbufw, u[i], v[i], samplerID.swizzle); u8 val = *src; res.v[i] = LookupColor(TransformClutIndex(val, samplerID), level, samplerID); } return res; case GE_TFMT_CLUT4: for (int i = 0; i < N; ++i) { const u8 *src = srcptr + GetPixelDataOffset<4>(texbufw, u[i], v[i], samplerID.swizzle); u8 val = (u[i] & 1) ? (src[0] >> 4) : (src[0] & 0xF); res.v[i] = LookupColor(TransformClutIndex(val, samplerID), level, samplerID); } return res; case GE_TFMT_DXT1: for (int i = 0; i < N; ++i) { const DXT1Block *block = (const DXT1Block *)srcptr + (v[i] >> 2) * (texbufw >> 2) + (u[i] >> 2); res.v[i] = GetDXT1Texel(block, u[i] & 3, v[i] & 3); } return res; case GE_TFMT_DXT3: for (int i = 0; i < N; ++i) { const DXT3Block *block = (const DXT3Block *)srcptr + (v[i] >> 2) * (texbufw >> 2) + (u[i] >> 2); res.v[i] = GetDXT3Texel(block, u[i] & 3, v[i] & 3); } return res; case GE_TFMT_DXT5: for (int i = 0; i < N; ++i) { const DXT5Block *block = (const DXT5Block *)srcptr + (v[i] >> 2) * (texbufw >> 2) + (u[i] >> 2); res.v[i] = GetDXT5Texel(block, u[i] & 3, v[i] & 3); } return res; default: ERROR_LOG_REPORT(Log::G3D, "Software: Unsupported texture format: %x", samplerID.TexFmt()); memset(res.v, 0, sizeof(res.v)); return res; } } static inline int ClampUV(int v, int height) { if (v >= height - 1) return height - 1; if (v >= 511) return 511; else if (v < 0) return 0; return v; } static inline int WrapUV(int v, int height) { return v & (height - 1) & 511; } template static inline void ApplyTexelClamp(int out_u[N], int out_v[N], const int u[N], const int v[N], int width, int height, const SamplerID &samplerID) { if (samplerID.clampS) { for (int i = 0; i < N; ++i) { out_u[i] = ClampUV(u[i], width); } } else { for (int i = 0; i < N; ++i) { out_u[i] = WrapUV(u[i], width); } } if (samplerID.clampT) { for (int i = 0; i < N; ++i) { out_v[i] = ClampUV(v[i], height); } } else { for (int i = 0; i < N; ++i) { out_v[i] = WrapUV(v[i], height); } } } // A texel coordinate in 1/16 texel, truncated. Out of int range it's INT_MIN, as the x86 sampler JIT's // conversion gives (gpu/probe exp5: s >= 2^18 on a 512 wide repeating texture reads texel 0 on a PSP). static inline int TexelFixed(float f) { return f >= -2147483648.0f && f < 2147483648.0f ? (int)f : INT_MIN; } static inline void GetTexelCoordinates(int level, float s, float t, int &out_u, int &out_v, const SamplerID &samplerID) { int width = samplerID.cached.sizes[level].w; int height = samplerID.cached.sizes[level].h; // The GE truncates to 1/16 texel, toward zero (gpu/probe exp57). int base_u = TexelFixed(s * width * 16.0f); int base_v = TexelFixed(t * height * 16.0f); base_u >>= 4; base_v >>= 4; ApplyTexelClamp<1>(&out_u, &out_v, &base_u, &base_v, width, height, samplerID); } Vec4IntResult SOFTRAST_CALL GetTextureFunctionOutput(Vec4IntArg prim_color_in, Vec4IntArg texcolor_in, const SamplerID &samplerID) { const Vec4 prim_color = prim_color_in; const Vec4 texcolor = texcolor_in; Vec3 out_rgb; int out_a; bool rgba = samplerID.useTextureAlpha; switch (samplerID.TexFunc()) { case GE_TEXFUNC_MODULATE: { #if defined(_M_SSE) // Modulate weights slightly on the tex color, by adding one to prim and dividing by 256. const __m128i p = _mm_slli_epi16(_mm_packs_epi32(prim_color.ivec, prim_color.ivec), 4); const __m128i pboost = _mm_add_epi16(p, _mm_set1_epi16(1 << 4)); __m128i t = _mm_slli_epi16(_mm_packs_epi32(texcolor.ivec, texcolor.ivec), 4); if (samplerID.useColorDoubling) { const __m128i amask = _mm_set_epi16(-1, 0, 0, 0, -1, 0, 0, 0); const __m128i a = _mm_and_si128(t, amask); const __m128i rgb = _mm_andnot_si128(amask, t); t = _mm_or_si128(_mm_slli_epi16(rgb, 1), a); } const __m128i b = _mm_mulhi_epi16(pboost, t); out_rgb.ivec = _mm_unpacklo_epi16(b, _mm_setzero_si128()); if (rgba) { return ToVec4IntResult(Vec4(out_rgb.ivec)); } else { out_a = prim_color.a(); } #elif PPSSPP_ARCH(ARM64_NEON) int32x4_t pboost = vaddq_s32(prim_color.ivec, vdupq_n_s32(1)); int32x4_t t = texcolor.ivec; if (samplerID.useColorDoubling) { static const int32_t rgbDouble[4] = { 1, 1, 1, 0 }; t = vshlq_s32(t, vld1q_s32(rgbDouble)); } out_rgb.ivec = vshrq_n_s32(vmulq_s32(pboost, t), 8); if (rgba) { return ToVec4IntResult(Vec4(out_rgb.ivec)); } out_a = prim_color.a(); #else if (samplerID.useColorDoubling) { out_rgb = ((prim_color.rgb() + Vec3::AssignToAll(1)) * texcolor.rgb() * 2) / 256; } else { out_rgb = (prim_color.rgb() + Vec3::AssignToAll(1)) * texcolor.rgb() / 256; } out_a = (rgba) ? ((prim_color.a() + 1) * texcolor.a() / 256) : prim_color.a(); #endif break; } case GE_TEXFUNC_DECAL: if (rgba) { int t = texcolor.a(); int invt = 255 - t; // Both colors are boosted here, making the alpha have more weight. Vec3 one = Vec3::AssignToAll(1); out_rgb = ((prim_color.rgb() + one) * invt + (texcolor.rgb() + one) * t); // Keep the bits of accuracy when doubling. if (samplerID.useColorDoubling) out_rgb /= 128; else out_rgb /= 256; } else { if (samplerID.useColorDoubling) out_rgb = texcolor.rgb() * 2; else out_rgb = texcolor.rgb(); } out_a = prim_color.a(); break; case GE_TEXFUNC_BLEND: { constexpr Vec3 const255(255, 255, 255); const Vec3 texenv = Vec3::FromRGB(samplerID.cached.texBlendColor); // Unlike the others (and even alpha), this one simply always rounds up. constexpr Vec3 roundup = Vec3::AssignToAll(255); out_rgb = ((const255 - texcolor.rgb()) * prim_color.rgb() + texcolor.rgb() * texenv + roundup); // Must divide by less to keep the precision for doubling to be accurate. if (samplerID.useColorDoubling) out_rgb /= 128; else out_rgb /= 256; out_a = (rgba) ? ((prim_color.a() + 1) * texcolor.a() / 256) : prim_color.a(); break; } case GE_TEXFUNC_REPLACE: out_rgb = texcolor.rgb(); // Doubling even happens for replace. if (samplerID.useColorDoubling) out_rgb *= 2; out_a = (rgba) ? texcolor.a() : prim_color.a(); break; case GE_TEXFUNC_ADD: case GE_TEXFUNC_UNKNOWN1: case GE_TEXFUNC_UNKNOWN2: case GE_TEXFUNC_UNKNOWN3: // Don't need to clamp afterward, we always clamp before tests. out_rgb = prim_color.rgb() + texcolor.rgb(); if (samplerID.useColorDoubling) out_rgb *= 2; // Alpha is still blended the common way. out_a = (rgba) ? ((prim_color.a() + 1) * texcolor.a() / 256) : prim_color.a(); break; } return ToVec4IntResult(Vec4(out_rgb, out_a)); } static Vec4IntResult SOFTRAST_CALL SampleNearest(float s, float t, Vec4IntArg prim_color, const u8 *const *tptr, const uint16_t *bufw, int level, int levelFrac, const SamplerID &samplerID) { int u, v; // Nearest filtering only. Round texcoords. GetTexelCoordinates(level, s, t, u, v, samplerID); Vec4 c0 = Vec4::FromRGBA(SampleNearest<1>(&u, &v, tptr[0], bufw[0], level, samplerID).v[0]); if (levelFrac) { GetTexelCoordinates(level + 1, s, t, u, v, samplerID); Vec4 c1 = Vec4::FromRGBA(SampleNearest<1>(&u, &v, tptr[1], bufw[1], level + 1, samplerID).v[0]); c0 = (c1 * levelFrac + c0 * (16 - levelFrac)) >> 4; } return GetTextureFunctionOutput(prim_color, ToVec4IntArg(c0), samplerID); } static Vec4IntResult SOFTRAST_CALL SampleFetch(int u, int v, const u8 *tptr, int bufw, int level, const SamplerID &samplerID) { Nearest4 c = SampleNearest<1>(&u, &v, tptr, bufw, level, samplerID); return ToVec4IntResult(Vec4::FromRGBA(c.v[0])); } static inline Vec4IntResult SOFTRAST_CALL ApplyTexelClampQuad(bool clamp, Vec4IntArg vec, int width) { Vec4 result = vec; #ifdef _M_SSE if (clamp) { // First, clamp to zero. __m128i negmask = _mm_cmpgt_epi32(_mm_setzero_si128(), result.ivec); result.ivec = _mm_andnot_si128(negmask, result.ivec); // Now the high bound. __m128i bound = _mm_set1_epi32(width > 512 ? 511 : width - 1); __m128i goodmask = _mm_cmpgt_epi32(bound, result.ivec); // Clear the ones that were too high, then or in the high bound to those. result.ivec = _mm_and_si128(goodmask, result.ivec); result.ivec = _mm_or_si128(result.ivec, _mm_andnot_si128(goodmask, bound)); } else { result.ivec = _mm_and_si128(result.ivec, _mm_set1_epi32((width - 1) & 511)); } #elif PPSSPP_ARCH(ARM64_NEON) if (clamp) { // Let's start by clamping to the maximum. result.ivec = vminq_s32(result.ivec, vdupq_n_s32(width > 512 ? 511 : width - 1)); // And then to zero. result.ivec = vmaxq_s32(result.ivec, vdupq_n_s32(0)); } else { result.ivec = vandq_s32(result.ivec, vdupq_n_s32((width - 1) & 511)); } #else if (clamp) { for (int i = 0; i < 4; ++i) { result[i] = ClampUV(result[i], width); } } else { for (int i = 0; i < 4; ++i) { result[i] = WrapUV(result[i], width); } } #endif return ToVec4IntResult(result); } static inline Vec4IntResult SOFTRAST_CALL ApplyTexelClampQuadS(bool clamp, int u, int width) { #ifdef _M_SSE __m128i uvec = _mm_add_epi32(_mm_set1_epi32(u), _mm_set_epi32(1, 0, 1, 0)); return ApplyTexelClampQuad(clamp, uvec, width); #elif PPSSPP_ARCH(ARM64_NEON) static const int32_t u2[4] = { 0, 1, 0, 1 }; int32x4_t uvec = vaddq_s32(vdupq_n_s32(u), vld1q_s32(u2)); return ApplyTexelClampQuad(clamp, uvec, width); #else Vec4 result = Vec4::AssignToAll(u) + Vec4(0, 1, 0, 1); return ApplyTexelClampQuad(clamp, ToVec4IntArg(result), width); #endif } static inline Vec4IntResult SOFTRAST_CALL ApplyTexelClampQuadT(bool clamp, int v, int height) { #ifdef _M_SSE __m128i vvec = _mm_add_epi32(_mm_set1_epi32(v), _mm_set_epi32(1, 1, 0, 0)); return ApplyTexelClampQuad(clamp, vvec, height); #elif PPSSPP_ARCH(ARM64_NEON) static const int32_t v2[4] = { 0, 0, 1, 1 }; int32x4_t vvec = vaddq_s32(vdupq_n_s32(v), vld1q_s32(v2)); return ApplyTexelClampQuad(clamp, vvec, height); #else Vec4 result = Vec4::AssignToAll(v) + Vec4(0, 0, 1, 1); return ApplyTexelClampQuad(clamp, ToVec4IntArg(result), height); #endif } static inline Vec4IntResult SOFTRAST_CALL GetTexelCoordinatesQuadS(int level, float in_s, int &frac_u, const SamplerID &samplerID) { int width = samplerID.cached.sizes[level].w; // The GE truncates to 1/16 texel, toward zero (gpu/probe exp57). int base_u = TexelFixed(in_s * width * 16) - 8; frac_u = base_u & 0x0F; base_u >>= 4; // Need to generate and individually wrap/clamp the four sample coordinates. Ugh. return ApplyTexelClampQuadS(samplerID.clampS, base_u, width); } static inline Vec4IntResult SOFTRAST_CALL GetTexelCoordinatesQuadT(int level, float in_t, int &frac_v, const SamplerID &samplerID) { int height = samplerID.cached.sizes[level].h; int base_v = TexelFixed(in_t * height * 16) - 8; frac_v = base_v & 0x0F; base_v >>= 4; // Need to generate and individually wrap/clamp the four sample coordinates. Ugh. return ApplyTexelClampQuadT(samplerID.clampT, base_v, height); } static Vec4IntResult SOFTRAST_CALL SampleLinearLevel(float s, float t, const u8 *const *tptr, const uint16_t *bufw, int texlevel, const SamplerID &samplerID) { int frac_u, frac_v; const Vec4 u = GetTexelCoordinatesQuadS(texlevel, s, frac_u, samplerID); const Vec4 v = GetTexelCoordinatesQuadT(texlevel, t, frac_v, samplerID); Nearest4 c = SampleNearest<4>(u.AsArray(), v.AsArray(), tptr[0], bufw[0], texlevel, samplerID); #ifdef _M_SSE __m128i zero = _mm_setzero_si128(); __m128i samples = _mm_loadu_si128((const __m128i*)(c.v)); __m128i top = _mm_unpacklo_epi8(samples, zero); __m128i bot = _mm_unpackhi_epi8(samples, zero); // I just a want reasonably efficient // __m128i mul_u = _mm_setr_epi16(0x10 - frac_u, 0x10 - frac_u, 0x10 - frac_u, 0x10 - frac_u, frac_u, frac_u, frac_u, frac_u); // GCC/clang do something decent for that, MSVC - not so much. // Hence this. (0x10 - frac_u) is expressed as (frac_u ^ 0xF) + 1, // which REQUIRES 0 <= frac_u < 0x10. __m128i mul_u = _mm_set1_epi16(frac_u); mul_u = _mm_xor_si128(mul_u, _mm_setr_epi16(0xF, 0xF, 0xF, 0xF, 0x0, 0x0, 0x0, 0x0)); mul_u = _mm_add_epi16(mul_u, _mm_setr_epi16(0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0)); // Like the GE: horizontal lerps truncated to 8 bits, then the vertical one (gpu/probe exp52). top = _mm_mullo_epi16(top, mul_u); bot = _mm_mullo_epi16(bot, mul_u); top = _mm_srli_epi16(_mm_add_epi16(top, _mm_shuffle_epi32(top, _MM_SHUFFLE(3, 2, 3, 2))), 4); bot = _mm_srli_epi16(_mm_add_epi16(bot, _mm_shuffle_epi32(bot, _MM_SHUFFLE(3, 2, 3, 2))), 4); top = _mm_mullo_epi16(top, _mm_set1_epi16(0x10 - frac_v)); bot = _mm_mullo_epi16(bot, _mm_set1_epi16(frac_v)); __m128i sum = _mm_srli_epi16(_mm_add_epi16(top, bot), 4); sum = _mm_unpacklo_epi16(sum, zero); return sum; #else Vec4 texcolor_tl = Vec4::FromRGBA(c.v[0]); Vec4 texcolor_tr = Vec4::FromRGBA(c.v[1]); Vec4 texcolor_bl = Vec4::FromRGBA(c.v[2]); Vec4 texcolor_br = Vec4::FromRGBA(c.v[3]); // Like the GE: horizontal lerps truncated to 8 bits, then the vertical one (gpu/probe exp52). Vec4 top = (texcolor_tl * (0x10 - frac_u) + texcolor_tr * frac_u) >> 4; Vec4 bot = (texcolor_bl * (0x10 - frac_u) + texcolor_br * frac_u) >> 4; return ToVec4IntResult((top * (0x10 - frac_v) + bot * frac_v) >> 4); #endif } static Vec4IntResult SOFTRAST_CALL SampleLinear(float s, float t, Vec4IntArg prim_color, const u8 *const *tptr, const uint16_t *bufw, int texlevel, int levelFrac, const SamplerID &samplerID) { Vec4 c0 = SampleLinearLevel(s, t, tptr, bufw, texlevel, samplerID); if (levelFrac) { const Vec4 c1 = SampleLinearLevel(s, t, tptr + 1, bufw + 1, texlevel + 1, samplerID); c0 = (c1 * levelFrac + c0 * (16 - levelFrac)) >> 4; } return GetTextureFunctionOutput(prim_color, ToVec4IntArg(c0), samplerID); } // Where there's no JIT: SampleLinear specialized for the texture format, swizzling and CLUT format, so // those aren't checked per texel. The filter works on all four channels at once, as 16-bit lanes of a // uint64_t (a channel times a weight of at most 16 fits). static inline uint64_t SpreadRGBA(uint32_t c) { uint64_t x = c; x = (x | (x << 16)) & 0x0000FFFF0000FFFFULL; return (x | (x << 8)) & 0x00FF00FF00FF00FFULL; } // (a * (16 - f) + b * f) >> 4 per channel. static inline uint64_t LerpSpread(uint64_t a, uint64_t b, int f) { return ((a * (uint64_t)(16 - f) + b * (uint64_t)f) >> 4) & 0x00FF00FF00FF00FFULL; } template static inline uint32_t LookupClutT(uint32_t index, const SamplerID &samplerID) { switch (clutFmt) { case GE_CMODE_16BIT_BGR5650: return RGB565ToRGBA8888(samplerID.cached.clut16[index]); case GE_CMODE_16BIT_ABGR5551: return RGBA5551ToRGBA8888(samplerID.cached.clut16[index]); case GE_CMODE_16BIT_ABGR4444: return RGBA4444ToRGBA8888(samplerID.cached.clut16[index]); default: return samplerID.cached.clut32[index]; } } template static constexpr uint32_t TexelBitsT() { switch (fmt) { case GE_TFMT_8888: case GE_TFMT_CLUT32: return 32; case GE_TFMT_CLUT8: return 8; case GE_TFMT_CLUT4: return 4; default: return 16; } } // A texel's byte offset is the sum of one for its row and one for its column (as GetPixelDataOffset), so // the four texels of a bilinear sample only need two of each. template static inline uint32_t RowOffsetT(int bufw, int v) { if (!swizzled) return v * (bufw * bits >> 3); // Blocks of 16 bytes by 8 rows. return (v >> 3) * ((bufw * bits / 32) * 32) + (v & 7) * 16; } template static inline uint32_t ColumnOffsetT(int u) { const uint32_t b = u * bits >> 3; return swizzled ? (b >> 4) * 128 + (b & 15) : b; } // The texel at p (u picks the nibble of CLUT4). clutOffset is where the level's palette starts. template static inline uint32_t ReadTexelT(const u8 *p, int u, uint32_t clutOffset, const SamplerID &samplerID) { switch (fmt) { case GE_TFMT_4444: return RGBA4444ToRGBA8888(*(const u16 *)p); case GE_TFMT_5551: return RGBA5551ToRGBA8888(*(const u16 *)p); case GE_TFMT_5650: return RGB565ToRGBA8888(*(const u16 *)p); case GE_TFMT_8888: return *(const u32 *)p; case GE_TFMT_CLUT32: return LookupClutT(TransformClutIndex(p[0] + (p[1] << 8) + (p[2] << 16) + (p[3] << 24), samplerID) + clutOffset, samplerID); case GE_TFMT_CLUT16: return LookupClutT(TransformClutIndex(p[0] + (p[1] << 8), samplerID) + clutOffset, samplerID); case GE_TFMT_CLUT8: return LookupClutT(TransformClutIndex(*p, samplerID) + clutOffset, samplerID); case GE_TFMT_CLUT4: return LookupClutT(TransformClutIndex((u & 1) ? (*p >> 4) : (*p & 0xF), samplerID) + clutOffset, samplerID); default: return 0; } } template static inline uint32_t ReadDXTTexelT(const u8 *src, int bufw, int u, int v) { switch (fmt) { case GE_TFMT_DXT1: return GetDXT1Texel((const DXT1Block *)src + (v >> 2) * (bufw >> 2) + (u >> 2), u & 3, v & 3); case GE_TFMT_DXT3: return GetDXT3Texel((const DXT3Block *)src + (v >> 2) * (bufw >> 2) + (u >> 2), u & 3, v & 3); default: return GetDXT5Texel((const DXT5Block *)src + (v >> 2) * (bufw >> 2) + (u >> 2), u & 3, v & 3); } } // The two texel coordinates along one axis (as ApplyTexelClampQuad wraps or clamps them) and the fraction. static inline void TexelPairT(float st, int size, bool clamp, int &c0, int &c1, int &frac) { int base = TexelFixed(st * size * 16) - 8; frac = base & 0x0F; base >>= 4; if (clamp) { const int hi = size > 512 ? 511 : size - 1; c0 = std::max(std::min(base, hi), 0); c1 = std::max(std::min(base + 1, hi), 0); } else { const int mask = (size - 1) & 511; c0 = base & mask; c1 = (base + 1) & mask; } } template static inline uint64_t SampleLinearLevelT(float s, float t, const u8 *tptr, int bufw, int level, const SamplerID &samplerID) { if (!tptr) return 0; int u0, u1, v0, v1, fracU, fracV; TexelPairT(s, samplerID.cached.sizes[level].w, samplerID.clampS, u0, u1, fracU); TexelPairT(t, samplerID.cached.sizes[level].h, samplerID.clampT, v0, v1, fracV); uint64_t tl, tr, bl, br; if constexpr (fmt == GE_TFMT_DXT1 || fmt == GE_TFMT_DXT3 || fmt == GE_TFMT_DXT5) { tl = SpreadRGBA(ReadDXTTexelT(tptr, bufw, u0, v0)); tr = SpreadRGBA(ReadDXTTexelT(tptr, bufw, u1, v0)); bl = SpreadRGBA(ReadDXTTexelT(tptr, bufw, u0, v1)); br = SpreadRGBA(ReadDXTTexelT(tptr, bufw, u1, v1)); } else { constexpr uint32_t bits = TexelBitsT(); // Levels past the first have their own palette unless it's shared: 16 entries on for CLUT4, 256 on for // CLUT8 at odd levels. uint32_t clutOffset = 0; if ((fmt == GE_TFMT_CLUT4 || fmt == GE_TFMT_CLUT8) && !samplerID.useSharedClut) clutOffset = fmt == GE_TFMT_CLUT4 ? level * 16 : (level & 1) * 256; const u8 *row0 = tptr + RowOffsetT(bufw, v0); const u8 *row1 = tptr + RowOffsetT(bufw, v1); const uint32_t col0 = ColumnOffsetT(u0); const uint32_t col1 = ColumnOffsetT(u1); tl = SpreadRGBA(ReadTexelT(row0 + col0, u0, clutOffset, samplerID)); tr = SpreadRGBA(ReadTexelT(row0 + col1, u1, clutOffset, samplerID)); bl = SpreadRGBA(ReadTexelT(row1 + col0, u0, clutOffset, samplerID)); br = SpreadRGBA(ReadTexelT(row1 + col1, u1, clutOffset, samplerID)); } // Like the GE: horizontal lerps truncated to 8 bits, then the vertical one (gpu/probe exp52). return LerpSpread(LerpSpread(tl, tr, fracU), LerpSpread(bl, br, fracU), fracV); } template static Vec4IntResult SOFTRAST_CALL SampleLinearT(float s, float t, Vec4IntArg prim_color, const u8 *const *tptr, const uint16_t *bufw, int texlevel, int levelFrac, const SamplerID &samplerID) { uint64_t c = SampleLinearLevelT(s, t, tptr[0], bufw[0], texlevel, samplerID); if (levelFrac) { const uint64_t c1 = SampleLinearLevelT(s, t, tptr[1], bufw[1], texlevel + 1, samplerID); c = LerpSpread(c, c1, levelFrac); } const Vec4 texcolor((int)(c & 0xFFFF), (int)((c >> 16) & 0xFFFF), (int)((c >> 32) & 0xFFFF), (int)(c >> 48)); return GetTextureFunctionOutput(prim_color, ToVec4IntArg(texcolor), samplerID); } // TexelPairT for four pixels at once. static inline void TexelPairs4(Vec4F32 st, int size, bool clamp, Vec4S32 &c0, Vec4S32 &c1, Vec4S32 &frac) { const Vec4F32 f = st * Vec4F32::Splat((float)size) * Vec4F32::Splat(16.0f); // TexelFixed: INT_MIN out of int range (and for NaN). Those lanes are zeroed before the conversion. const Vec4S32 inRange = f.CompareLt(Vec4F32::Splat(2147483648.0f)).AndNot(f.CompareLt(Vec4F32::Splat(-2147483648.0f))); const Vec4S32 fixed = Vec4S32FromF32(f & inRange) | Vec4S32::Splat(INT_MIN).AndNot(inRange); const Vec4S32 base = fixed - Vec4S32::Splat(8); frac = base & Vec4S32::Splat(0x0F); const Vec4S32 b0 = base.Shr<4>(); const Vec4S32 b1 = b0 + Vec4S32::Splat(1); if (clamp) { const Vec4S32 hi = Vec4S32::Splat(size > 512 ? 511 : size - 1); c0 = b0.Min(hi).Max(Vec4S32::Zero()); c1 = b1.Min(hi).Max(Vec4S32::Zero()); } else { const Vec4S32 mask = Vec4S32::Splat((size - 1) & 511); c0 = b0 & mask; c1 = b1 & mask; } } // v times k, both from 0 to 32767 for Mul16 (a 32-bit multiply is slow on SSE2). static inline Vec4S32 MulRow(Vec4S32 v, int k) { return k < 32768 ? v.Mul16(Vec4S32::Splat(k)) : v.Mul(Vec4S32::Splat(k)); } // RowOffsetT and ColumnOffsetT for four texels at once. The rows are below 512. template static inline Vec4S32 RowOffsets4(int bufw, Vec4S32 v) { if (!swizzled) return MulRow(v, bufw * bits >> 3); return MulRow(v.Shr<3>(), bufw * bits / 32).Shl<5>() + (v & Vec4S32::Splat(7)).Shl<4>(); } template static inline Vec4S32 ColumnOffsets4(Vec4S32 u) { Vec4S32 b; switch (bits) { case 32: b = u.Shl<2>(); break; case 16: b = u.Shl<1>(); break; case 8: b = u; break; default: b = u.Shr<1>(); break; } return swizzled ? b.Shr<4>().Shl<7>() + (b & Vec4S32::Splat(15)) : b; } // The four texels of SampleLinearLevelT for four pixels at the same level (texels[tl, tr, bl, br][pixel]), // the coordinates and offsets in vector lanes, the texel reads one by one. template static inline void ReadTexels4T(Vec4F32 s, Vec4F32 t, const u8 *tptr, int bufw, int level, const SamplerID &samplerID, uint32_t texels[4][4], Vec4S32 &fracU, Vec4S32 &fracV) { Vec4S32 u0, u1, v0, v1; TexelPairs4(s, samplerID.cached.sizes[level].w, samplerID.clampS, u0, u1, fracU); TexelPairs4(t, samplerID.cached.sizes[level].h, samplerID.clampT, v0, v1, fracV); alignas(16) int us0[4], us1[4]; u0.Store(us0); u1.Store(us1); if constexpr (fmt == GE_TFMT_DXT1 || fmt == GE_TFMT_DXT3 || fmt == GE_TFMT_DXT5) { alignas(16) int vs0[4], vs1[4]; v0.Store(vs0); v1.Store(vs1); for (int i = 0; i < 4; ++i) { texels[0][i] = ReadDXTTexelT(tptr, bufw, us0[i], vs0[i]); texels[1][i] = ReadDXTTexelT(tptr, bufw, us1[i], vs0[i]); texels[2][i] = ReadDXTTexelT(tptr, bufw, us0[i], vs1[i]); texels[3][i] = ReadDXTTexelT(tptr, bufw, us1[i], vs1[i]); } } else { constexpr uint32_t bits = TexelBitsT(); uint32_t clutOffset = 0; if ((fmt == GE_TFMT_CLUT4 || fmt == GE_TFMT_CLUT8) && !samplerID.useSharedClut) clutOffset = fmt == GE_TFMT_CLUT4 ? level * 16 : (level & 1) * 256; const Vec4S32 row0 = RowOffsets4(bufw, v0), row1 = RowOffsets4(bufw, v1); const Vec4S32 col0 = ColumnOffsets4(u0), col1 = ColumnOffsets4(u1); alignas(16) int tl[4], tr[4], bl[4], br[4]; (row0 + col0).Store(tl); (row0 + col1).Store(tr); (row1 + col0).Store(bl); (row1 + col1).Store(br); for (int i = 0; i < 4; ++i) { texels[0][i] = ReadTexelT(tptr + (uint32_t)tl[i], us0[i], clutOffset, samplerID); texels[1][i] = ReadTexelT(tptr + (uint32_t)tr[i], us1[i], clutOffset, samplerID); texels[2][i] = ReadTexelT(tptr + (uint32_t)bl[i], us0[i], clutOffset, samplerID); texels[3][i] = ReadTexelT(tptr + (uint32_t)br[i], us1[i], clutOffset, samplerID); } } } // LerpSpread of one channel in vector lanes. static inline Vec4S32 LerpLanes(Vec4S32 a, Vec4S32 b, Vec4S32 f) { return (a * (Vec4S32::Splat(16) - f) + b * f).Shr<4>(); } // SampleLinearLevelT for four pixels at the same level, a channel per vector (as TextureFunction4 takes them). template static inline void SampleLinearLevel4LanesT(Vec4F32 s, Vec4F32 t, const u8 *tptr, int bufw, int level, const SamplerID &samplerID, Vec4S32 out[4]) { if (!tptr) { out[0] = out[1] = out[2] = out[3] = Vec4S32::Zero(); return; } alignas(16) uint32_t texels[4][4]; Vec4S32 fracU, fracV; ReadTexels4T(s, t, tptr, bufw, level, samplerID, texels, fracU, fracV); // Two channels per lane, R and B (G and A) as 16-bit halves: no lerp of 8-bit values exceeds 16 bits, // so the halves don't carry into each other, and masking after each shift truncates both to 8 bits. const Vec4S32 tl = Vec4S32::LoadAligned((const int *)texels[0]), tr = Vec4S32::LoadAligned((const int *)texels[1]); const Vec4S32 bl = Vec4S32::LoadAligned((const int *)texels[2]), br = Vec4S32::LoadAligned((const int *)texels[3]); const Vec4S32 halves = Vec4S32::Splat(0x00FF00FF); // The fractions in both halves, for MulHalves: no product exceeds 16 bits either. const Vec4S32 sixteen = Vec4S32::Splat(0x00100010); const Vec4S32 fracU2 = fracU | fracU.Shl<16>(), fracV2 = fracV | fracV.Shl<16>(); const Vec4S32 invU = sixteen - fracU2, invV = sixteen - fracV2; fracU = fracU2; fracV = fracV2; auto lerp = [&](Vec4S32 a, Vec4S32 b, Vec4S32 inv, Vec4S32 f) { return (a.MulHalves(inv) + b.MulHalves(f)).Shr<4>() & halves; }; Vec4S32 pairs[2]; for (int h = 0; h < 2; ++h) { auto part = [&](Vec4S32 v) { return (h == 0 ? v : v.Shr<8>()) & halves; }; // Like the GE: horizontal lerps truncated to 8 bits, then the vertical one (gpu/probe exp52). const Vec4S32 top = lerp(part(tl), part(tr), invU, fracU); const Vec4S32 bot = lerp(part(bl), part(br), invU, fracU); pairs[h] = lerp(top, bot, invV, fracV); } const Vec4S32 byteMask = Vec4S32::Splat(0xFF); out[0] = pairs[0] & byteMask; out[1] = pairs[1] & byteMask; out[2] = pairs[0].Shr<16>(); out[3] = pairs[1].Shr<16>(); } // GetTextureFunctionOutput for four pixels, one per lane: prim and tex are R, G, B and A (all 0 to 255). static inline void TextureFunction4(const Vec4S32 prim[4], const Vec4S32 tex[4], const SamplerID &samplerID, Vec4S32 out[4]) { const bool rgba = samplerID.useTextureAlpha; const bool doubling = samplerID.useColorDoubling; const Vec4S32 one = Vec4S32::Splat(1), c255 = Vec4S32::Splat(255); // The alpha blended the common way: (prim + 1) * tex / 256. const Vec4S32 modulatedA = rgba ? (prim[3] + one).Mul16(tex[3]).Shr<8>() : prim[3]; switch (samplerID.TexFunc()) { case GE_TEXFUNC_MODULATE: for (int c = 0; c < 3; ++c) out[c] = (prim[c] + one).Mul16(doubling ? tex[c].Shl<1>() : tex[c]).Shr<8>(); out[3] = modulatedA; break; case GE_TEXFUNC_DECAL: if (rgba) { // Both colors are boosted here, making the alpha have more weight. const Vec4S32 t = tex[3], invt = c255 - tex[3]; for (int c = 0; c < 3; ++c) { const Vec4S32 sum = (prim[c] + one).Mul16(invt) + (tex[c] + one).Mul16(t); out[c] = doubling ? sum.Shr<7>() : sum.Shr<8>(); } } else { for (int c = 0; c < 3; ++c) out[c] = doubling ? tex[c].Shl<1>() : tex[c]; } out[3] = prim[3]; break; case GE_TEXFUNC_BLEND: { // Unlike the others (and even alpha), this one always rounds up. const uint32_t env = samplerID.cached.texBlendColor; for (int c = 0; c < 3; ++c) { const Vec4S32 sum = (c255 - tex[c]).Mul16(prim[c]) + tex[c].Mul16(Vec4S32::Splat((env >> (8 * c)) & 0xFF)) + c255; out[c] = doubling ? sum.Shr<7>() : sum.Shr<8>(); } out[3] = modulatedA; break; } case GE_TEXFUNC_REPLACE: for (int c = 0; c < 3; ++c) out[c] = doubling ? tex[c].Shl<1>() : tex[c]; out[3] = rgba ? tex[3] : prim[3]; break; default: // ADD and the unknown ones. for (int c = 0; c < 3; ++c) out[c] = doubling ? (prim[c] + tex[c]).Shl<1>() : prim[c] + tex[c]; out[3] = modulatedA; break; } } template static void SOFTRAST_CALL SampleLinearQuadT(const float *s, const float *t, const int *level, const int *levelFrac, int active, const u8 *const *texptr, const uint16_t *texbufw, int *colors, int colorStride, const SamplerID &samplerID) { if (!active) return; uint64_t c[4]; // Usually all at one level (a span of a triangle always is): then in vector lanes. No level arrays: all at // level 0, without a fraction. const int first = active & 1 ? 0 : active & 2 ? 1 : active & 4 ? 2 : 3; bool sameLevel = true; if (level) { for (int i = first + 1; i < 4; ++i) { if ((active & (1 << i)) && (level[i] != level[first] || levelFrac[i] != levelFrac[first])) sameLevel = false; } } if (sameLevel) { const int l = level ? level[first] : 0; const Vec4F32 sv = Vec4F32::Load(s), tv = Vec4F32::Load(t); Vec4S32 prim[4], tex[4], out[4]; SampleLinearLevel4LanesT(sv, tv, texptr[l], texbufw[l], l, samplerID, tex); if (level && levelFrac[first]) { Vec4S32 tex1[4]; SampleLinearLevel4LanesT(sv, tv, texptr[l + 1], texbufw[l + 1], l + 1, samplerID, tex1); const Vec4S32 f = Vec4S32::Splat(levelFrac[first]); for (int ch = 0; ch < 4; ++ch) tex[ch] = LerpLanes(tex[ch], tex1[ch], f); } // The texture function, in lanes too. for (int ch = 0; ch < 4; ++ch) prim[ch] = Vec4S32::Load(colors + ch * colorStride); TextureFunction4(prim, tex, samplerID, out); alignas(16) static const int laneBits[4] = { 1, 2, 4, 8 }; const Vec4S32 keep = (Vec4S32::Splat(active) & Vec4S32::LoadAligned(laneBits)).CompareEq(Vec4S32::Zero()); for (int ch = 0; ch < 4; ++ch) (out[ch].AndNot(keep) | (prim[ch] & keep)).Store(colors + ch * colorStride); return; } // The four samples' loads and arithmetic are independent, so they can overlap. for (int i = 0; i < 4; ++i) { if (!(active & (1 << i))) continue; const int l = level[i]; c[i] = SampleLinearLevelT(s[i], t[i], texptr[l], texbufw[l], l, samplerID); if (levelFrac[i]) c[i] = LerpSpread(c[i], SampleLinearLevelT(s[i], t[i], texptr[l + 1], texbufw[l + 1], l + 1, samplerID), levelFrac[i]); } for (int i = 0; i < 4; ++i) { if (!(active & (1 << i))) continue; const Vec4 texcolor((int)(c[i] & 0xFFFF), (int)((c[i] >> 16) & 0xFFFF), (int)((c[i] >> 32) & 0xFFFF), (int)(c[i] >> 48)); const Vec4 prim(colors[i], colors[colorStride + i], colors[2 * colorStride + i], colors[3 * colorStride + i]); const Vec4 out = GetTextureFunctionOutput(ToVec4IntArg(prim), ToVec4IntArg(texcolor), samplerID); for (int ch = 0; ch < 4; ++ch) colors[ch * colorStride + i] = out[ch]; } } template static LinearQuadFunc PickLinearQuadClut(GEPaletteFormat clutFmt) { switch (clutFmt) { case GE_CMODE_16BIT_BGR5650: return &SampleLinearQuadT; case GE_CMODE_16BIT_ABGR5551: return &SampleLinearQuadT; case GE_CMODE_16BIT_ABGR4444: return &SampleLinearQuadT; default: return &SampleLinearQuadT; } } template static LinearQuadFunc PickLinearQuad(const SamplerID &id) { switch (fmt) { case GE_TFMT_CLUT4: case GE_TFMT_CLUT8: case GE_TFMT_CLUT16: case GE_TFMT_CLUT32: return id.swizzle ? PickLinearQuadClut(id.ClutFmt()) : PickLinearQuadClut(id.ClutFmt()); case GE_TFMT_DXT1: case GE_TFMT_DXT3: case GE_TFMT_DXT5: return &SampleLinearQuadT; default: return id.swizzle ? &SampleLinearQuadT : &SampleLinearQuadT; } } LinearQuadFunc GetLinearQuadFunc(const SamplerID &id, LinearFunc linear) { if (linear != GetLinearFallback(id)) return nullptr; switch (id.TexFmt()) { case GE_TFMT_5650: return PickLinearQuad(id); case GE_TFMT_5551: return PickLinearQuad(id); case GE_TFMT_4444: return PickLinearQuad(id); case GE_TFMT_8888: return PickLinearQuad(id); case GE_TFMT_CLUT4: return PickLinearQuad(id); case GE_TFMT_CLUT8: return PickLinearQuad(id); case GE_TFMT_CLUT16: return PickLinearQuad(id); case GE_TFMT_CLUT32: return PickLinearQuad(id); case GE_TFMT_DXT1: return PickLinearQuad(id); case GE_TFMT_DXT3: return PickLinearQuad(id); case GE_TFMT_DXT5: return PickLinearQuad(id); default: return nullptr; } } template static LinearFunc PickLinearClut(GEPaletteFormat clutFmt) { switch (clutFmt) { case GE_CMODE_16BIT_BGR5650: return &SampleLinearT; case GE_CMODE_16BIT_ABGR5551: return &SampleLinearT; case GE_CMODE_16BIT_ABGR4444: return &SampleLinearT; default: return &SampleLinearT; } } template static LinearFunc PickLinearDirect(bool swizzled) { return swizzled ? &SampleLinearT : &SampleLinearT; } template static LinearFunc PickLinearIndexed(const SamplerID &id) { return id.swizzle ? PickLinearClut(id.ClutFmt()) : PickLinearClut(id.ClutFmt()); } static LinearFunc GetLinearFallback(const SamplerID &id) { switch (id.TexFmt()) { case GE_TFMT_5650: return PickLinearDirect(id.swizzle); case GE_TFMT_5551: return PickLinearDirect(id.swizzle); case GE_TFMT_4444: return PickLinearDirect(id.swizzle); case GE_TFMT_8888: return PickLinearDirect(id.swizzle); case GE_TFMT_CLUT4: return PickLinearIndexed(id); case GE_TFMT_CLUT8: return PickLinearIndexed(id); case GE_TFMT_CLUT16: return PickLinearIndexed(id); case GE_TFMT_CLUT32: return PickLinearIndexed(id); case GE_TFMT_DXT1: return &SampleLinearT; case GE_TFMT_DXT3: return &SampleLinearT; case GE_TFMT_DXT5: return &SampleLinearT; default: return &SampleLinear; } } };