Files
Joe Mattiello 907d15155e rend: NEON for the 0888 and 8888 framebuffer write paths on arm64 (#2557)
0888 KRGB and 8888 ARGB, still went through the scalar per-pixel writer.
One packed pixel is exactly one VRAM word, so they fit the same 8-word
interleaved store as the 16-bit path, 8 pixels per iteration. The store
is factored out of storeFramebuffer16Pixels() so both use it.

888 (24-bit packed) stays scalar: its pixels straddle VRAM words, so the
store pattern only repeats every 4 pixels / 3 words.

The address walk is the same as writeFramebufferLW(), so the output is
identical to the scalar path, including when the padding isn't a
multiple of 4. A harness writing 7,056 frames through both paths (both
channel orders, modes 4 to 6, 6 frame sizes, 7 destination addresses
including bank and VRAM edges, 7 line strides, 4 clip windows) gives
byte-identical VRAM. The same algorithm measured about 2x faster on a
640x480 8888 frame (0.213 ms to 0.105 ms) on Apple Silicon.

tests/src/rend/FramebufferWriteTest.cpp checks both 32-bit modes against
an independent reference (one pvr_write32p per pixel, the scalar walk)
over 3,600 cases: widths around the 8-pixel vector width, padding
including values that aren't a multiple of 4, destinations at the 32-bit
bank boundary and the end of VRAM, and clip windows, for both channel
orders. All of VRAM is compared, so a store past the frame fails too. On
arm64 it exercises the NEON path; elsewhere the scalar one. It fails when
the padding computation is deliberately broken.

Other architectures are unchanged.

Co-authored-by: Claude Opus 5.5 <[email protected]>
[ci skip]
2026-10-26 10:26:27 +01:00

1783 lines
51 KiB
C++

/*
This file is part of Flycast.
Flycast 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, either version 2 of the License, or
(at your option) any later version.
Flycast 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 for more details.
You should have received a copy of the GNU General Public License
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
*/
#include "TexCache.h"
#include "xbrz/xbrz.h"
#include "hw/pvr/pvr_mem.h"
#include "hw/mem/addrspace.h"
#include <mutex>
#include <new>
#include <type_traits>
#include <xxhash.h>
#if HOST_CPU == CPU_ARM64
#include <arm_neon.h>
#endif
#ifdef _OPENMP
#include <omp.h>
#endif
extern bool pal_needs_update;
// Rough approximation of LoD bias from D adjust param, only used to increase LoD
const std::array<f32, 16> D_Adjust_LoD_Bias = {
0.f, -4.f, -2.f, -1.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f
};
static std::vector<vram_block*> *VramLocks;
static inline void initVramLocks() {
if (VramLocks == nullptr)
VramLocks = new std::vector<vram_block*>[VRAM_SIZE_MAX / PAGE_SIZE];
}
//List functions
//
static void vramlock_list_remove(vram_block* block)
{
u32 base = block->start / PAGE_SIZE;
u32 end = block->end / PAGE_SIZE;
for (u32 i = base; i <= end; i++)
{
std::vector<vram_block*>& list = VramLocks[i];
for (auto& lock : list)
{
if (lock == block)
lock = nullptr;
}
}
}
static void vramlock_list_add(vram_block* block)
{
u32 base = block->start / PAGE_SIZE;
u32 end = block->end / PAGE_SIZE;
for (u32 i = base; i <= end; i++)
{
std::vector<vram_block*>& list = VramLocks[i];
// If the list is empty then we need to protect vram, otherwise it's already been done
if (list.empty() || std::all_of(list.begin(), list.end(), [](vram_block *block) { return block == nullptr; }))
addrspace::protectVram(i * PAGE_SIZE, PAGE_SIZE);
auto it = std::find(list.begin(), list.end(), nullptr);
if (it != list.end())
*it = block;
else
list.push_back(block);
}
}
static std::mutex vramlist_lock;
bool VramLockedWriteOffset(size_t offset)
{
if (offset >= VRAM_SIZE || VramLocks == nullptr)
return false;
size_t addr_hash = offset / PAGE_SIZE;
std::vector<vram_block *>& list = VramLocks[addr_hash];
{
std::lock_guard<std::mutex> lockguard(vramlist_lock);
for (auto& lock : list)
{
if (lock != nullptr)
{
lock->texture->invalidate();
if (lock != nullptr)
{
ERROR_LOG(PVR, "Error : pvr is supposed to remove lock");
die("Invalid state");
}
}
}
list.clear();
addrspace::unprotectVram((u32)(offset & ~PAGE_MASK), PAGE_SIZE);
}
return true;
}
bool VramLockedWrite(u8* address)
{
u32 offset = addrspace::getVramOffset(address);
if (offset == (u32)-1)
return false;
return VramLockedWriteOffset(offset);
}
//unlocks mem
//also frees the handle
static void libCore_vramlock_Unlock_block_wb(vram_block* block)
{
vramlock_list_remove(block);
delete block;
}
#ifdef _OPENMP
static inline int getThreadCount()
{
int tcount = omp_get_num_procs() - 1;
if (tcount < 1)
tcount = 1;
return std::min(tcount, (int)config::MaxThreads);
}
template<typename Func>
void parallelize(Func func, int start, int end)
{
int tcount = getThreadCount();
#pragma omp parallel num_threads(tcount)
{
int num_threads = omp_get_num_threads();
int thread = omp_get_thread_num();
int chunk = (end - start) / num_threads;
func(start + chunk * thread,
num_threads == thread + 1 ? end
: (start + chunk * (thread + 1)));
}
}
#endif
static struct xbrz::ScalerCfg xbrz_cfg;
void UpscalexBRZ(int factor, u32* source, u32* dest, int width, int height, bool has_alpha)
{
#ifdef _OPENMP
parallelize([=](int start, int end) {
xbrz::scale(factor, source, dest, width, height, has_alpha ? xbrz::ColorFormat::ARGB : xbrz::ColorFormat::RGB,
xbrz_cfg, start, end);
}, 0, height);
#else
xbrz::scale(factor, source, dest, width, height, has_alpha ? xbrz::ColorFormat::ARGB : xbrz::ColorFormat::RGB, xbrz_cfg);
#endif
}
extern const u32 VQMipPoint[11] =
{
VQ_CODEBOOK_SIZE + 0x00000, // 1
VQ_CODEBOOK_SIZE + 0x00001, // 2
VQ_CODEBOOK_SIZE + 0x00002, // 4
VQ_CODEBOOK_SIZE + 0x00006, // 8
VQ_CODEBOOK_SIZE + 0x00016, // 16
VQ_CODEBOOK_SIZE + 0x00056, // 32
VQ_CODEBOOK_SIZE + 0x00156, // 64
VQ_CODEBOOK_SIZE + 0x00556, // 128
VQ_CODEBOOK_SIZE + 0x01556, // 256
VQ_CODEBOOK_SIZE + 0x05556, // 512
VQ_CODEBOOK_SIZE + 0x15556 // 1024
};
extern const u32 OtherMipPoint[11] =
{
0x00003,//1
0x00004,//2
0x00008,//4
0x00018,//8
0x00058,//16
0x00158,//32
0x00558,//64
0x01558,//128
0x05558,//256
0x15558,//512
0x55558//1024
};
static const TextureType PAL_TYPE[4] = {
TextureType::_5551, TextureType::_565, TextureType::_4444, TextureType::_8888
};
void BaseTextureCacheData::PrintTextureName()
{
#if !defined(NDEBUG) || defined(DEBUGFAST)
char str[512];
snprintf(str, sizeof(str), "Texture: %s", GetPixelFormatName());
if (tcw.VQ_Comp)
strcat(str, " VQ");
else if (tcw.ScanOrder == 0 || IsPaletted())
strcat(str, " TW");
else if (tcw.StrideSel == 1 && !IsPaletted())
strcat(str, " Stride");
if (tcw.ScanOrder == 0 && tcw.MipMapped)
strcat(str, " MM");
if (tsp.FilterMode != 0)
strcat(str, " Bilinear");
size_t len = strlen(str);
snprintf(str + len, sizeof(str) - len, " %dx%d @ 0x%X", 8 << tsp.TexU, 8 << tsp.TexV, tcw.TexAddr << 3);
std::string id = GetId();
len = strlen(str);
snprintf(str + len, sizeof(str) - len, " id=%s", id.c_str());
DEBUG_LOG(RENDERER, "%s", str);
#endif
}
//true if : dirty or paletted texture and hashes don't match
bool BaseTextureCacheData::NeedsUpdate() {
bool rc = dirty != 0;
if (tex_type != TextureType::_8)
{
if (tcw.PixelFmt == PixelPal4 && palette_hash != pal_hash_16[tcw.PalSelect])
rc = true;
else if (tcw.PixelFmt == PixelPal8 && palette_hash != pal_hash_256[tcw.PalSelect >> 4])
rc = true;
}
return rc;
}
void BaseTextureCacheData::protectVRam()
{
u32 end = mmStartAddress + size - 1;
if (end >= VRAM_SIZE)
{
WARN_LOG(PVR, "protectVRam: end >= VRAM_SIZE. Tried to lock area out of vram");
end = VRAM_SIZE - 1;
}
if (startAddress > end)
{
WARN_LOG(PVR, "vramlock_Lock: startAddress > end. Tried to lock negative block");
return;
}
vram_block *block = new vram_block();
block->end = end;
block->start = startAddress;
block->texture = this;
{
std::lock_guard<std::mutex> lock(vramlist_lock);
if (lock_block == nullptr)
{
// This also protects vram if needed
vramlock_list_add(block);
lock_block = block;
}
else
delete block;
}
}
void BaseTextureCacheData::unprotectVRam()
{
std::lock_guard<std::mutex> lock(vramlist_lock);
if (lock_block)
libCore_vramlock_Unlock_block_wb(lock_block);
lock_block = nullptr;
}
bool BaseTextureCacheData::Delete()
{
unprotectVRam();
custom_texture.cancelRequest(customRequestId);
customRequestId = {};
customPayload.reset();
gpuPreloadedTexture.reset();
usingGpuPreloadedTexture = false;
customMipLevels = 0;
return true;
}
BaseTextureCacheData::BaseTextureCacheData(TSP tsp, TCW tcw, int area)
{
initVramLocks();
if (tcw.VQ_Comp == 1 && tcw.MipMapped == 1)
// Star Wars Demolition
tcw.ScanOrder = 0;
this->tsp = tsp;
this->tcw = tcw;
this->area = area;
//Reset state info ..
Updates = 0;
dirty = FrameCount;
lock_block = nullptr;
customPayload.reset();
gpuPreloadedTexture.reset();
customRequestId = {};
gpuPalette = false;
is_custom_replaced = false;
usingGpuPreloadedTexture = false;
customMipLevels = 0;
//decode info from tsp/tcw into the texture struct
tex = &pvrTexInfo[tcw.PixelFmt == PixelReserved ? Pixel1555 : tcw.PixelFmt]; //texture format table entry
startAddress = (tcw.TexAddr << 3) & VRAM_MASK; // texture start address
mmStartAddress = startAddress; // data texture start address (modified for MIPs, as needed)
width = 8 << tsp.TexU; //tex width
height = 8 << tsp.TexV; //tex height
texconv8 = nullptr;
if (tcw.ScanOrder && tex->PL32 != nullptr)
{
//Texture is stored 'planar' in memory, no deswizzle is needed
if (tcw.MipMapped != 0)
{
WARN_LOG(RENDERER, "Warning: planar texture with mipmaps (invalid)");
this->tcw.MipMapped = 0;
}
//Planar textures support stride selection, mostly used for non power of 2 textures (videos)
int stride = width;
if (tcw.StrideSel)
{
stride = (TEXT_CONTROL & 31) * 32;
if (stride == 0)
stride = width;
}
//Call the format specific conversion code
texconv = nullptr;
if (tcw.VQ_Comp != 0)
{
// VQ
texconv32 = tex->PLVQ32;
mmStartAddress += VQ_CODEBOOK_SIZE;
size = stride * height / 4;
}
else
{
// Normal
texconv32 = tex->PL32;
//calculate the size, in bytes, for the locking
size = stride * height * tex->bpp / 8;
}
}
else
{
if (!IsPaletted())
{
this->tcw.ScanOrder = 0;
this->tcw.StrideSel = 0;
}
// Quake 3 Arena uses one
if (tcw.MipMapped)
// Mipmapped texture must be square and TexV is ignored
height = width;
if (tcw.VQ_Comp)
{
verify(tex->VQ != NULL || tex->VQ32 != NULL);
if (tcw.MipMapped)
mmStartAddress += VQMipPoint[tsp.TexU + 3];
else
mmStartAddress += VQ_CODEBOOK_SIZE;
texconv = tex->VQ;
texconv32 = tex->VQ32;
size = width * height / 4;
}
else
{
verify(tex->TW != NULL || tex->TW32 != NULL);
if (tcw.MipMapped)
mmStartAddress += OtherMipPoint[tsp.TexU + 3] * tex->bpp / 8;
texconv = tex->TW;
texconv32 = tex->TW32;
size = width * height * tex->bpp / 8;
texconv8 = tex->TW8;
}
}
}
void BaseTextureCacheData::ComputeHash()
{
// Include everything but texaddr, reserved and stride. Palette textures don't have ScanOrder
const u32 tcwMask = IsPaletted() ? 0xF8000000 : 0xFC000000;
if (tcw.VQ_Comp)
{
// The size for VQ textures wasn't correctly calculated.
// We use the old size to compute the hash for backward-compatibility
// with existing custom texture packs.
int oldSize = width * height / 8;
old_vqtexture_hash = XXH32(&vram[mmStartAddress - VQ_CODEBOOK_SIZE], oldSize, 7);
if (IsPaletted())
old_vqtexture_hash ^= palette_hash;
old_texture_hash = old_vqtexture_hash;
old_vqtexture_hash ^= tcw.full & tcwMask;
// New hash
XXH32_state_t *state = XXH32_createState();
XXH32_reset(state, 7);
// hash vq codebook
XXH32_update(state, &vram[startAddress], VQ_CODEBOOK_SIZE);
// hash texture
XXH32_update(state, &vram[mmStartAddress], size);
texture_hash = XXH32_digest(state);
XXH32_freeState(state);
if (IsPaletted())
texture_hash ^= palette_hash;
texture_hash ^= tcw.full & tcwMask;
}
else
{
old_vqtexture_hash = 0;
texture_hash = XXH32(&vram[mmStartAddress], size, 7);
if (IsPaletted())
texture_hash ^= palette_hash;
old_texture_hash = texture_hash;
texture_hash ^= tcw.full & tcwMask;
}
}
bool BaseTextureCacheData::Update()
{
//texture state tracking stuff
Updates++;
dirty = 0;
gpuPalette = false;
tex_type = tex->type;
const u8 *vqCodebook = nullptr;
u32 paletteIndex = 0;
bool has_alpha = false;
if (IsPaletted())
{
if (IsGpuHandledPaletted(tsp, tcw, area))
{
tex_type = TextureType::_8;
gpuPalette = true;
}
else
{
tex_type = PAL_TYPE[PAL_RAM_CTRL&3];
if (tex_type != TextureType::_565)
has_alpha = true;
}
// Get the palette hash to check for future updates
if (tcw.PixelFmt == PixelPal4)
{
palette_hash = pal_hash_16[tcw.PalSelect];
paletteIndex = tcw.PalSelect << 4;
}
else
{
palette_hash = pal_hash_256[tcw.PalSelect >> 4];
paletteIndex = (tcw.PalSelect >> 4) << 8;
}
}
if (tcw.VQ_Comp)
vqCodebook = &vram[startAddress];
//texture conversion work
u32 stride = width;
if (tcw.StrideSel && tcw.ScanOrder && tex->PL32 != nullptr)
{
stride = (TEXT_CONTROL & 31) * 32;
if (stride == 0)
stride = width;
}
u32 heightLimit = height;
const u32 originalSize = size;
if (startAddress > VRAM_SIZE || mmStartAddress + size > VRAM_SIZE)
{
heightLimit = 0;
if (mmStartAddress < VRAM_SIZE && mmStartAddress + size > VRAM_SIZE && tcw.ScanOrder)
{
// Shenmue Space Harrier mini-arcade loads a texture that goes beyond the end of VRAM
// but only uses the top portion of it
heightLimit = (VRAM_SIZE - mmStartAddress) * 8 / stride / tex->bpp;
size = stride * heightLimit * tex->bpp/8;
}
if (heightLimit == 0)
{
size = originalSize;
WARN_LOG(RENDERER, "Warning: invalid texture. Address %08X %08X size %d", startAddress, mmStartAddress, size);
dirty = 1;
unprotectVRam();
return false;
}
}
bool dumpReplacedTexture = false;
if (custom_texture.enabled())
{
u32 oldHash = texture_hash;
ComputeHash();
if (Updates > 1 && oldHash == texture_hash)
{
// Texture hasn't changed so skip the update.
if (is_custom_replaced)
{
tex_type = TextureType::_8888;
gpuPalette = false;
}
protectVRam();
size = originalSize;
return true;
}
gpuPreloadedTexture = renderer == nullptr ? nullptr
: renderer->findGpuPreloadedTexture(texture_hash,
old_vqtexture_hash, old_texture_hash);
if (gpuPreloadedTexture)
{
custom_texture.cancelRequest(customRequestId);
customRequestId = {};
customPayload.reset();
}
else
custom_texture.loadCustomTextureAsync(this);
if (gpuPreloadedTexture || customPayload)
{
dumpReplacedTexture = config::DumpTextures && config::DumpReplacedTextures.get();
if (!dumpReplacedTexture)
{
const TextureType originalTextureType = tex_type;
if (CheckCustomTexture())
{
protectVRam();
PrintTextureName();
size = originalSize;
return true;
}
tex_type = originalTextureType;
}
}
}
is_custom_replaced = false;
customMipLevels = 0;
void *temp_tex_buffer = nullptr;
u32 upscaled_w = width;
u32 upscaled_h = height;
PixelBuffer<u16> pb16;
PixelBuffer<u32> pb32;
PixelBuffer<u8> pb8;
// Figure out if we really need to use a 32-bit pixel buffer
bool textureUpscaling = config::TextureUpscale > 1
// Don't process textures that are too big
&& (int)(width * height) <= config::MaxFilteredTextureSize * config::MaxFilteredTextureSize
// Don't process YUV textures
&& tcw.PixelFmt != PixelYUV;
bool need_32bit_buffer = true;
if (!textureUpscaling
&& (!IsPaletted() || tex_type != TextureType::_8888)
&& texconv != NULL
&& !Force32BitTexture(tex_type))
need_32bit_buffer = false;
// TODO avoid upscaling/depost. textures that change too often
bool mipmapped = IsMipmapped() && !config::DumpTextures;
if (texconv32 != NULL && need_32bit_buffer)
{
if (textureUpscaling)
// don't use mipmaps if upscaling
mipmapped = false;
// Force the texture type since that's the only 32-bit one we know
tex_type = TextureType::_8888;
pb32.setVQCodebook(vqCodebook);
pb32.setPaletteIndex(paletteIndex);
if (mipmapped)
{
pb32.init(width, height, true);
for (u32 i = 0; i <= tsp.TexU + 3u; i++)
{
pb32.set_mipmap(i);
u32 vram_addr;
if (tcw.VQ_Comp)
{
vram_addr = startAddress + VQMipPoint[i];
if (i == 0)
{
PixelBuffer<u32> pb0;
pb0.init(2, 2 ,false);
pb0.setVQCodebook(vqCodebook);
pb0.setPaletteIndex(paletteIndex);
if (tcw.PixelFmt == PixelYUV)
// Use higher LoD mipmap
vram_addr = startAddress + VQMipPoint[1];
texconv32(&pb0, &vram[vram_addr], 2, 2);
*pb32.data() = *pb0.data(1, 1);
continue;
}
}
else
vram_addr = startAddress + OtherMipPoint[i] * tex->bpp / 8;
if (tcw.PixelFmt == PixelYUV && i == 0)
// Special case for YUV at 1x1 LoD
pvrTexInfo[Pixel565].TW32(&pb32, &vram[vram_addr], 1, 1);
else
texconv32(&pb32, &vram[vram_addr], 1 << i, 1 << i);
}
pb32.set_mipmap(0);
}
else
{
pb32.init(width, height);
texconv32(&pb32, (u8*)&vram[mmStartAddress], stride, heightLimit);
// xBRZ scaling
if (textureUpscaling)
{
// xBRZ only supports scale factors 2 to 6. Out of range, scale() returns
// without writing the destination buffer.
const int factor = std::clamp((int)config::TextureUpscale, 2, 6);
PixelBuffer<u32> tmp_buf;
tmp_buf.init(width * factor, height * factor);
if (tcw.PixelFmt == Pixel1555 || tcw.PixelFmt == Pixel4444)
// Alpha channel formats. Palettes with alpha are already handled
has_alpha = true;
UpscalexBRZ(factor, pb32.data(), tmp_buf.data(), width, height, has_alpha);
pb32.steal_data(tmp_buf);
upscaled_w *= factor;
upscaled_h *= factor;
}
}
temp_tex_buffer = pb32.data();
}
else if (texconv8 != NULL && tex_type == TextureType::_8)
{
pb8.setVQCodebook(vqCodebook);
pb8.setPaletteIndex(paletteIndex);
if (mipmapped)
{
// This shouldn't happen since mipmapped palette textures are converted to rgba
pb8.init(width, height, true);
for (u32 i = 0; i <= tsp.TexU + 3u; i++)
{
pb8.set_mipmap(i);
u32 vram_addr = startAddress + OtherMipPoint[i] * tex->bpp / 8;
texconv8(&pb8, &vram[vram_addr], 1 << i, 1 << i);
}
pb8.set_mipmap(0);
}
else
{
pb8.init(width, height);
texconv8(&pb8, &vram[mmStartAddress], stride, height);
}
temp_tex_buffer = pb8.data();
}
else if (texconv != NULL)
{
pb16.setVQCodebook(vqCodebook);
pb16.setPaletteIndex(paletteIndex);
if (mipmapped)
{
pb16.init(width, height, true);
for (u32 i = 0; i <= tsp.TexU + 3u; i++)
{
pb16.set_mipmap(i);
u32 vram_addr;
if (tcw.VQ_Comp)
{
vram_addr = startAddress + VQMipPoint[i];
if (i == 0)
{
PixelBuffer<u16> pb0;
pb0.init(2, 2 ,false);
pb0.setVQCodebook(vqCodebook);
pb0.setPaletteIndex(paletteIndex);
texconv(&pb0, (u8*)&vram[vram_addr], 2, 2);
*pb16.data() = *pb0.data(1, 1);
continue;
}
}
else
vram_addr = startAddress + OtherMipPoint[i] * tex->bpp / 8;
texconv(&pb16, (u8*)&vram[vram_addr], 1 << i, 1 << i);
}
pb16.set_mipmap(0);
}
else
{
pb16.init(width, height);
texconv(&pb16, (u8*)&vram[mmStartAddress], stride, heightLimit);
}
temp_tex_buffer = pb16.data();
}
else
{
//fill it in with a temp color
WARN_LOG(RENDERER, "UNHANDLED TEXTURE");
pb16.init(width, height);
memset(pb16.data(), 0x80, width * height * 2);
temp_tex_buffer = pb16.data();
mipmapped = false;
}
//lock the texture to detect changes in it
protectVRam();
bool customTextureUploaded = false;
if (dumpReplacedTexture)
{
ComputeHash();
custom_texture.dumpTexture(this, upscaled_w, upscaled_h, temp_tex_buffer);
NOTICE_LOG(RENDERER, "Dumped texture %x.png. Old hash %x", texture_hash, old_texture_hash);
const TextureType originalTextureType = tex_type;
customTextureUploaded = CheckCustomTexture();
if (!customTextureUploaded)
tex_type = originalTextureType;
}
if (!customTextureUploaded)
UploadToGPU(upscaled_w, upscaled_h, (const u8 *)temp_tex_buffer, IsMipmapped(), mipmapped);
if (config::DumpTextures && !dumpReplacedTexture)
{
ComputeHash();
custom_texture.dumpTexture(this, upscaled_w, upscaled_h, temp_tex_buffer);
NOTICE_LOG(RENDERER, "Dumped texture %x.png. Old hash %x", texture_hash, old_texture_hash);
}
PrintTextureName();
// Restore the original texture size if it was constrained to VRAM limits above
size = originalSize;
return true;
}
bool BaseTextureCacheData::uploadCustomTexture(const PreparedCustomTexture& texture, bool mipmapped)
{
if (texture.nativeFormat != NativeTextureFormat::Rgba8Unorm || texture.levels.size() != 1)
return false;
tex_type = TextureType::_8888;
UploadToGPU(texture.width, texture.height, texture.bytes.data(),
mipmapped && texture.generateMipmaps, false);
return true;
}
bool BaseTextureCacheData::CheckCustomTexture()
{
if (gpuPreloadedTexture)
{
if (!useGpuPreloadedTexture(gpuPreloadedTexture))
{
WARN_LOG(RENDERER, "Could not use GPU-preloaded custom texture");
custom_texture.reportError(CustomTextureException::Error::Upload);
custom_texture.showErrorNotification();
gpuPreloadedTexture.reset();
return false;
}
tex_type = TextureType::_8888;
gpuPalette = false;
is_custom_replaced = true;
usingGpuPreloadedTexture = true;
customMipLevels = gpuPreloadedTexture->mipLevels;
return true;
}
if (!customPayload && customRequestId)
{
bool failed = false;
customPayload = custom_texture.takePreparedTexture(customRequestId, failed);
if (failed)
customRequestId = {};
}
if (!customPayload)
return false;
const CustomTextureCapabilities capabilities = custom_texture.getCapabilities();
if (!capabilities.canUpload(customPayload->nativeFormat, customPayload->width,
customPayload->height, static_cast<u32>(customPayload->levels.size())))
{
WARN_LOG(RENDERER, "Discarding incompatible prepared custom texture %08x",
customPayload->replacementHash);
custom_texture.reportError(customPayload->width > capabilities.max2DWidth
|| customPayload->height > capabilities.max2DHeight
? CustomTextureException::Error::TextureTooLarge
: CustomTextureException::Error::Upload);
custom_texture.showErrorNotification();
customPayload.reset();
customRequestId = {};
return false;
}
bool uploaded = false;
bool uploadFailureLogged = false;
const bool mipmapped = tcw.MipMapped != 0 && tcw.ScanOrder == 0 && config::UseMipmaps;
try
{
uploaded = uploadCustomTexture(*customPayload, mipmapped);
}
catch (const FlycastException& exception)
{
WARN_LOG(RENDERER, "Custom texture validation failed for %08x (%s): %s",
customPayload->replacementHash,
nativeTextureFormatName(customPayload->nativeFormat), exception.what());
uploadFailureLogged = true;
}
catch (const std::bad_alloc& exception)
{
WARN_LOG(RENDERER, "Custom texture upload allocation failed for %08x (%s): %s",
customPayload->replacementHash,
nativeTextureFormatName(customPayload->nativeFormat), exception.what());
uploadFailureLogged = true;
}
if (!uploaded)
{
if (!uploadFailureLogged)
WARN_LOG(RENDERER, "Custom texture upload failed for %08x (%s)",
customPayload->replacementHash, nativeTextureFormatName(customPayload->nativeFormat));
custom_texture.reportError(CustomTextureException::Error::Upload);
custom_texture.showErrorNotification();
customPayload.reset();
customRequestId = {};
return false;
}
tex_type = TextureType::_8888;
gpuPalette = false;
is_custom_replaced = true;
customMipLevels = static_cast<u8>(mipmapped && customPayload->generateMipmaps
? mipmapLevelCount(customPayload->width, customPayload->height)
: customPayload->levels.size());
customPayload.reset();
customRequestId = {};
return true;
}
void BaseTextureCacheData::SetDirectXColorOrder(bool enabled) {
pvrTexInfo = enabled ? directx::pvrTexInfo : opengl::pvrTexInfo;
pal_needs_update = true;
}
#if HOST_CPU == CPU_ARM64
static inline bool pvr32SpanIsContiguous(u32 addr, u32 logicalBytes)
{
const u32 maskedAddr = addr & VRAM_MASK;
if ((maskedAddr & 3) != 0
|| logicalBytes > VRAM_32_BANK_BIT
|| (maskedAddr & (VRAM_32_BANK_BIT - 1)) > VRAM_32_BANK_BIT - logicalBytes
|| maskedAddr > VRAM_MASK - (logicalBytes - 1))
return false;
// The 32-bit bus doubles the offset and adds bank * 4. NEON deinterleaving
// loads therefore access twice the logical span, including the other bank.
return (u64)pvr_map32(maskedAddr) + (u64)logicalBytes * 2 <= (u64)vram.getSize();
}
template<typename Packer>
static inline void storeFramebufferRgba(u32 *dst, uint8x16_t red, uint8x16_t green, uint8x16_t blue)
{
uint8x16x4_t rgba;
if constexpr (std::is_same_v<Packer, RGBAPacker>)
{
rgba.val[0] = red;
rgba.val[2] = blue;
}
else
{
rgba.val[0] = blue;
rgba.val[2] = red;
}
rgba.val[1] = green;
rgba.val[3] = vdupq_n_u8(0xff);
vst4q_u8((u8 *)dst, rgba);
}
template<typename Packer, bool RGB565>
static void readFramebuffer16Line(u32 addr, u32 *dst, int width, u32 fb_concat)
{
int x = 0;
for (; x < width && (addr & 3) != 0; x++, addr += 2)
{
const u16 src = pvr_read32p<u16>(addr);
if constexpr (RGB565)
dst[x] = Packer::pack((((src >> 11) & 0x1f) << 3) | fb_concat,
(((src >> 5) & 0x3f) << 2) | (fb_concat & 3),
((src & 0x1f) << 3) | fb_concat, 0xff);
else
dst[x] = Packer::pack((((src >> 10) & 0x1f) << 3) | fb_concat,
(((src >> 5) & 0x1f) << 3) | fb_concat,
((src & 0x1f) << 3) | fb_concat, 0xff);
}
for (; x + 16 <= width && pvr32SpanIsContiguous(addr, 32); x += 16, addr += 32)
{
const u8 *mapped = &vram[pvr_map32(addr)];
const uint16x8_t src0 = vreinterpretq_u16_u32(vld2q_u32((const u32 *)mapped).val[0]);
const uint16x8_t src1 = vreinterpretq_u16_u32(vld2q_u32((const u32 *)(mapped + 32)).val[0]);
const uint16x8_t concat5 = vdupq_n_u16(fb_concat);
uint16x8_t red0;
uint16x8_t red1;
uint16x8_t green0;
uint16x8_t green1;
if constexpr (RGB565)
{
red0 = vorrq_u16(vshlq_n_u16(vshrq_n_u16(src0, 11), 3), concat5);
red1 = vorrq_u16(vshlq_n_u16(vshrq_n_u16(src1, 11), 3), concat5);
const uint16x8_t concat6 = vdupq_n_u16(fb_concat & 3);
green0 = vorrq_u16(vshlq_n_u16(vandq_u16(vshrq_n_u16(src0, 5), vdupq_n_u16(0x3f)), 2), concat6);
green1 = vorrq_u16(vshlq_n_u16(vandq_u16(vshrq_n_u16(src1, 5), vdupq_n_u16(0x3f)), 2), concat6);
}
else
{
red0 = vorrq_u16(vshlq_n_u16(vandq_u16(vshrq_n_u16(src0, 10), vdupq_n_u16(0x1f)), 3), concat5);
red1 = vorrq_u16(vshlq_n_u16(vandq_u16(vshrq_n_u16(src1, 10), vdupq_n_u16(0x1f)), 3), concat5);
green0 = vorrq_u16(vshlq_n_u16(vandq_u16(vshrq_n_u16(src0, 5), vdupq_n_u16(0x1f)), 3), concat5);
green1 = vorrq_u16(vshlq_n_u16(vandq_u16(vshrq_n_u16(src1, 5), vdupq_n_u16(0x1f)), 3), concat5);
}
const uint16x8_t blue0 = vorrq_u16(vshlq_n_u16(vandq_u16(src0, vdupq_n_u16(0x1f)), 3), concat5);
const uint16x8_t blue1 = vorrq_u16(vshlq_n_u16(vandq_u16(src1, vdupq_n_u16(0x1f)), 3), concat5);
storeFramebufferRgba<Packer>(dst + x,
vcombine_u8(vmovn_u16(red0), vmovn_u16(red1)),
vcombine_u8(vmovn_u16(green0), vmovn_u16(green1)),
vcombine_u8(vmovn_u16(blue0), vmovn_u16(blue1)));
}
for (; x < width; x++, addr += 2)
{
const u16 src = pvr_read32p<u16>(addr);
if constexpr (RGB565)
dst[x] = Packer::pack((((src >> 11) & 0x1f) << 3) | fb_concat,
(((src >> 5) & 0x3f) << 2) | (fb_concat & 3),
((src & 0x1f) << 3) | fb_concat, 0xff);
else
dst[x] = Packer::pack((((src >> 10) & 0x1f) << 3) | fb_concat,
(((src >> 5) & 0x1f) << 3) | fb_concat,
((src & 0x1f) << 3) | fb_concat, 0xff);
}
}
template<typename Packer>
static void readFramebufferC888Line(u32 addr, u32 *dst, int width)
{
int x = 0;
for (; x + 8 <= width && pvr32SpanIsContiguous(addr, 32); x += 8, addr += 32)
{
const u8 *mapped = &vram[pvr_map32(addr)];
const uint32x4_t src0 = vld2q_u32((const u32 *)mapped).val[0];
const uint32x4_t src1 = vld2q_u32((const u32 *)(mapped + 32)).val[0];
const uint8x8_t red = vmovn_u16(vcombine_u16(
vmovn_u32(vshrq_n_u32(src0, 16)), vmovn_u32(vshrq_n_u32(src1, 16))));
const uint8x8_t green = vmovn_u16(vcombine_u16(
vmovn_u32(vshrq_n_u32(src0, 8)), vmovn_u32(vshrq_n_u32(src1, 8))));
const uint8x8_t blue = vmovn_u16(vcombine_u16(vmovn_u32(src0), vmovn_u32(src1)));
uint8x8x4_t rgba;
if constexpr (std::is_same_v<Packer, RGBAPacker>)
{
rgba.val[0] = red;
rgba.val[2] = blue;
}
else
{
rgba.val[0] = blue;
rgba.val[2] = red;
}
rgba.val[1] = green;
rgba.val[3] = vdup_n_u8(0xff);
vst4_u8((u8 *)(dst + x), rgba);
}
for (; x < width; x++, addr += 4)
{
const u32 src = pvr_read32p<u32>(addr);
dst[x] = Packer::pack(src >> 16, src >> 8, src, 0xff);
}
}
#endif
template<typename Packer>
void ReadFramebuffer(const FramebufferInfo& info, PixelBuffer<u32>& pb, int& width, int& height)
{
width = (info.fb_r_size.fb_x_size + 1) * 2; // in 16-bit words
height = info.fb_r_size.fb_y_size + 1;
int modulus = (info.fb_r_size.fb_modulus - 1) * 2;
int bpp;
switch (info.fb_r_ctrl.fb_depth)
{
case fbde_0555:
case fbde_565:
bpp = 2;
break;
case fbde_888:
bpp = 3;
width = (width * 2) / 3; // in pixels
modulus = (modulus * 2) / 3; // in pixels
break;
case fbde_C888:
bpp = 4;
width /= 2; // in pixels
modulus /= 2; // in pixels
break;
default:
die("Invalid framebuffer format\n");
bpp = 4;
break;
}
u32 addr = info.fb_r_sof1;
if (info.spg_control.interlace)
{
if (width == modulus && info.fb_r_sof2 == info.fb_r_sof1 + width * bpp)
{
// Typical case alternating even and odd lines -> take the whole buffer at once
modulus = 0;
height *= 2;
}
else
{
addr = info.spg_status.fieldnum ? info.fb_r_sof2 : info.fb_r_sof1;
}
}
else if (info.fb_r_ctrl.vclk_div == 0) {
// 240p if PAL or NTSC
height = std::min(height, 240);
}
pb.init(width, height);
u32 *dst = (u32 *)pb.data();
const u32 fb_concat = info.fb_r_ctrl.fb_concat;
switch (info.fb_r_ctrl.fb_depth)
{
case fbde_0555: // 555 RGB
for (int y = 0; y < height; y++)
{
#if HOST_CPU == CPU_ARM64
readFramebuffer16Line<Packer, false>(addr, dst, width, fb_concat);
dst += width;
addr += (width + modulus) * bpp;
#else
for (int i = 0; i < width; i++)
{
u16 src = pvr_read32p<u16>(addr);
*dst++ = Packer::pack(
(((src >> 10) & 0x1F) << 3) | fb_concat,
(((src >> 5) & 0x1F) << 3) | fb_concat,
(((src >> 0) & 0x1F) << 3) | fb_concat,
0xff);
addr += bpp;
}
addr += modulus * bpp;
#endif
}
break;
case fbde_565: // 565 RGB
for (int y = 0; y < height; y++)
{
#if HOST_CPU == CPU_ARM64
readFramebuffer16Line<Packer, true>(addr, dst, width, fb_concat);
dst += width;
addr += (width + modulus) * bpp;
#else
for (int i = 0; i < width; i++)
{
u16 src = pvr_read32p<u16>(addr);
*dst++ = Packer::pack(
(((src >> 11) & 0x1F) << 3) | fb_concat,
(((src >> 5) & 0x3F) << 2) | (fb_concat & 3),
(((src >> 0) & 0x1F) << 3) | fb_concat,
0xFF);
addr += bpp;
}
addr += modulus * bpp;
#endif
}
break;
case fbde_888: // 888 RGB
for (int y = 0; y < height; y++)
{
for (int i = 0; i < width; i += 4)
{
u32 src = pvr_read32p<u32>(addr);
*dst++ = Packer::pack(src >> 16, src >> 8, src, 0xff);
addr += 4;
if (i + 1 >= width)
break;
u32 src2 = pvr_read32p<u32>(addr);
*dst++ = Packer::pack(src2 >> 8, src2, src >> 24, 0xff);
addr += 4;
if (i + 2 >= width)
break;
u32 src3 = pvr_read32p<u32>(addr);
*dst++ = Packer::pack(src3, src2 >> 24, src2 >> 16, 0xff);
addr += 4;
if (i + 3 >= width)
break;
*dst++ = Packer::pack(src3 >> 24, src3 >> 16, src3 >> 8, 0xff);
}
addr += modulus * bpp;
}
break;
case fbde_C888: // 0888 RGB
for (int y = 0; y < height; y++)
{
#if HOST_CPU == CPU_ARM64
readFramebufferC888Line<Packer>(addr, dst, width);
dst += width;
addr += (width + modulus) * bpp;
#else
for (int i = 0; i < width; i++)
{
u32 src = pvr_read32p<u32>(addr);
*dst++ = Packer::pack(src >> 16, src >> 8, src, 0xff);
addr += bpp;
}
addr += modulus * bpp;
#endif
}
break;
}
}
template void ReadFramebuffer<RGBAPacker>(const FramebufferInfo& info, PixelBuffer<u32>& pb, int& width, int& height);
template void ReadFramebuffer<BGRAPacker>(const FramebufferInfo& info, PixelBuffer<u32>& pb, int& width, int& height);
template<int bits>
static inline u8 roundColor(u8 in)
{
u8 out = in >> (8 - bits);
if (out != 0xffu >> (8 - bits))
out += (in >> (8 - bits - 1)) & 1;
return out;
}
// write to 32-bit vram area (framebuffer)
class FBPixelWriter
{
public:
FBPixelWriter(u32 dstAddr) : dstAddr(dstAddr) {}
template<typename T>
void write(T pixel) {
pvr_write32p<T, true>(dstAddr, pixel);
dstAddr += sizeof(T);
}
void advance(int bytes) {
dstAddr += bytes;
}
private:
u32 dstAddr;
};
// write to 64-bit vram area (render to texture)
class TexPixelWriter
{
public:
TexPixelWriter(u16 *dest) : dest(dest) {}
void write(u16 pixel) {
*dest++ = pixel;
}
void advance(int bytes) {
(u8 *&)dest += bytes;
}
private:
u16 *dest;
};
// 0555 KRGB 16 bit (default) Bit 15 is the value of fb_kval[7].
template<int Red, int Green, int Blue, int Alpha, typename PixelWriter, bool Round = false>
class FBLineWriter0555
{
public:
FBLineWriter0555(FB_W_CTRL_type fb_w_ctrl, PixelWriter& pixWriter) : pixWriter(pixWriter) {
kval_bit = (fb_w_ctrl.fb_kval & 0x80) << 8;
}
void write(int xmin, int xmax, const u8 *& pixel)
{
for (int c = xmin; c < xmax; c++)
{
u8 red, green, blue;
if constexpr (Round)
{
red = roundColor<5>(pixel[Red]);
green = roundColor<5>(pixel[Green]);
blue = roundColor<5>(pixel[Blue]);
}
else
{
red = pixel[Red] >> 3;
green = pixel[Green] >> 3;
blue = pixel[Blue] >> 3;
}
pixWriter.write((u16)((red << 10) | (green << 5) | blue | kval_bit));
pixel += 4;
}
}
static constexpr int BytesPerPixel = 2;
private:
u16 kval_bit;
PixelWriter& pixWriter;
};
// 565 RGB 16 bit
template<int Red, int Green, int Blue, int Alpha, typename PixelWriter, bool Round = false>
class FBLineWriter565
{
public:
FBLineWriter565(FB_W_CTRL_type fb_w_ctrl, PixelWriter& pixWriter) : pixWriter(pixWriter) {}
void write(int xmin, int xmax, const u8 *& pixel)
{
for (int c = xmin; c < xmax; c++)
{
u8 red, green, blue;
if constexpr (Round)
{
red = roundColor<5>(pixel[Red]);
green = roundColor<6>(pixel[Green]);
blue = roundColor<5>(pixel[Blue]);
}
else
{
red = pixel[Red] >> 3;
green = pixel[Green] >> 2;
blue = pixel[Blue] >> 3;
}
pixWriter.write((u16)((red << 11) | (green << 5) | blue));
pixel += 4;
}
}
static constexpr int BytesPerPixel = 2;
private:
PixelWriter& pixWriter;
};
// 4444 ARGB 16 bit
template<int Red, int Green, int Blue, int Alpha, typename PixelWriter, bool Round = false>
class FBLineWriter4444
{
public:
FBLineWriter4444(FB_W_CTRL_type fb_w_ctrl, PixelWriter& pixWriter) : pixWriter(pixWriter) {}
void write(int xmin, int xmax, const u8 *& pixel)
{
for (int c = xmin; c < xmax; c++)
{
u8 red, green, blue, alpha;
if constexpr (Round)
{
red = roundColor<4>(pixel[Red]);
green = roundColor<4>(pixel[Green]);
blue = roundColor<4>(pixel[Blue]);
alpha = roundColor<4>(pixel[Alpha]);
}
else
{
red = pixel[Red] >> 4;
green = pixel[Green] >> 4;
blue = pixel[Blue] >> 4;
alpha = pixel[Alpha] >> 4;
}
pixWriter.write((u16)((red << 8) | (green << 4) | blue | (alpha << 12)));
pixel += 4;
}
}
static constexpr int BytesPerPixel = 2;
private:
PixelWriter& pixWriter;
};
// 1555 ARGB 16 bit The alpha value is determined by comparison with the value of fb_alpha_threshold.
template<int Red, int Green, int Blue, int Alpha, typename PixelWriter, bool Round = false>
class FBLineWriter1555
{
public:
FBLineWriter1555(FB_W_CTRL_type fb_w_ctrl, PixelWriter& pixWriter) : pixWriter(pixWriter) {
fb_alpha_threshold = fb_w_ctrl.fb_alpha_threshold;
}
void write(int xmin, int xmax, const u8 *& pixel)
{
for (int c = xmin; c < xmax; c++)
{
u8 red, green, blue;
if constexpr (Round)
{
red = roundColor<5>(pixel[Red]);
green = roundColor<5>(pixel[Green]);
blue = roundColor<5>(pixel[Blue]);
}
else
{
red = pixel[Red] >> 3;
green = pixel[Green] >> 3;
blue = pixel[Blue] >> 3;
}
pixWriter.write((u16)((red << 10) | (green << 5) | blue
| (pixel[Alpha] >= fb_alpha_threshold ? 0x8000 : 0)));
pixel += 4;
}
}
static constexpr int BytesPerPixel = 2;
private:
PixelWriter& pixWriter;
u8 fb_alpha_threshold;
};
// 888 RGB 24 bit packed
template<int Red, int Green, int Blue, int Alpha, typename PixelWriter>
class FBLineWriter888
{
public:
FBLineWriter888(FB_W_CTRL_type fb_w_ctrl, PixelWriter& pixWriter) : pixWriter(pixWriter) {}
void write(int xmin, int xmax, const u8 *& pixel)
{
for (int c = xmin; c < xmax; c++)
{
pixWriter.write(pixel[Blue]);
pixWriter.write(pixel[Green]);
pixWriter.write(pixel[Red]);
pixel += 4;
}
}
static constexpr int BytesPerPixel = 3;
private:
PixelWriter& pixWriter;
};
// 0888 KRGB 32 bit (K is the value of fb_kval.)
template<int Red, int Green, int Blue, int Alpha, typename PixelWriter>
class FBLineWriter0888
{
public:
FBLineWriter0888(FB_W_CTRL_type fb_w_ctrl, PixelWriter& pixWriter) : pixWriter(pixWriter) {
fb_kval = fb_w_ctrl.fb_kval << 24;
}
void write(int xmin, int xmax, const u8 *& pixel)
{
for (int c = xmin; c < xmax; c++)
{
pixWriter.write((u32)((pixel[Red] << 16) | (pixel[Green] << 8) | pixel[Blue] | fb_kval));
pixel += 4;
}
}
static constexpr int BytesPerPixel = 4;
private:
u32 fb_kval;
PixelWriter& pixWriter;
};
// 8888 ARGB 32 bit
template<int Red, int Green, int Blue, int Alpha, typename PixelWriter>
class FBLineWriter8888
{
public:
FBLineWriter8888(FB_W_CTRL_type fb_w_ctrl, PixelWriter& pixWriter) : pixWriter(pixWriter) {}
void write(int xmin, int xmax, const u8 *& pixel)
{
for (int c = xmin; c < xmax; c++)
{
pixWriter.write((u32)((pixel[Red] << 16) | (pixel[Green] << 8) | pixel[Blue] | (pixel[Alpha] << 24)));
pixel += 4;
}
}
static constexpr int BytesPerPixel = 4;
private:
PixelWriter& pixWriter;
};
template<typename FBLineWriter>
static void writeTexture(u32 width, u32 height, const u8 *data, u16 *dst, FB_W_CTRL_type fb_w_ctrl, u32 linestride, const Rect& clip)
{
int srcPadding = 0;
int destPadding = 0;
if (linestride == 0)
linestride = width * 2;
else if (linestride > width * 2u)
destPadding = linestride - width * 2;
else
srcPadding = (width * 2 - linestride) * 2;
height = std::min<u32>(height, clip.bottomRight().y + 1);
const int xmin = clip.origin.x;
int xmax = std::min<int>(clip.bottomRight().x + 1, width);
xmax = std::min<int>(xmax, linestride / 2);
TexPixelWriter pixWriter(dst + clip.origin.y * (width + destPadding / 2) + xmin);
data += clip.origin.y * (width * 2 + srcPadding) + xmin * 2;
const int deltaPix = xmin + std::min(linestride / 2, width) - xmax;
destPadding += deltaPix * 2;
srcPadding += deltaPix * 4;
FBLineWriter lineWriter(fb_w_ctrl, pixWriter);
for (u32 l = clip.origin.y; l < height; l++)
{
lineWriter.write(xmin, xmax, data);
pixWriter.advance(destPadding);
data += srcPadding;
}
}
template<int Red, int Green, int Blue, int Alpha>
void WriteTextureToVRam(u32 width, u32 height, const u8 *data, u16 *dst, FB_W_CTRL_type fb_w_ctrl, u32 linestride, const Rect& clip)
{
bool dither = fb_w_ctrl.fb_dither && config::EmulateFramebuffer;
switch (fb_w_ctrl.fb_packmode)
{
case 0: // 0555 KRGB 16 bit (default)
if (dither)
writeTexture<FBLineWriter0555<Red, Green, Blue, Alpha, TexPixelWriter, false>>(width, height, data, dst, fb_w_ctrl, linestride, clip);
else
writeTexture<FBLineWriter0555<Red, Green, Blue, Alpha, TexPixelWriter, true>>(width, height, data, dst, fb_w_ctrl, linestride, clip);
break;
case 1: // 565 RGB 16 bit
if (dither)
writeTexture<FBLineWriter565<Red, Green, Blue, Alpha, TexPixelWriter, false>>(width, height, data, dst, fb_w_ctrl, linestride, clip);
else
writeTexture<FBLineWriter565<Red, Green, Blue, Alpha, TexPixelWriter, true>>(width, height, data, dst, fb_w_ctrl, linestride, clip);
break;
case 2: // 4444 ARGB 16 bit
if (dither)
writeTexture<FBLineWriter4444<Red, Green, Blue, Alpha, TexPixelWriter, false>>(width, height, data, dst, fb_w_ctrl, linestride, clip);
else
writeTexture<FBLineWriter4444<Red, Green, Blue, Alpha, TexPixelWriter, true>>(width, height, data, dst, fb_w_ctrl, linestride, clip);
break;
case 3: // 1555 ARGB 16 bit
if (dither)
writeTexture<FBLineWriter1555<Red, Green, Blue, Alpha, TexPixelWriter, false>>(width, height, data, dst, fb_w_ctrl, linestride, clip);
else
writeTexture<FBLineWriter1555<Red, Green, Blue, Alpha, TexPixelWriter, true>>(width, height, data, dst, fb_w_ctrl, linestride, clip);
break;
default:
ERROR_LOG(PVR, "Invalid/unsupported texture format: %d", fb_w_ctrl.fb_packmode);
break;
}
}
template void WriteTextureToVRam<0, 1, 2, 3>(u32 width, u32 height, const u8 *data, u16 *dst, FB_W_CTRL_type fb_w_ctrl, u32 linestride, const Rect& clip);
template void WriteTextureToVRam<2, 1, 0, 3>(u32 width, u32 height, const u8 *data, u16 *dst, FB_W_CTRL_type fb_w_ctrl, u32 linestride, const Rect& clip);
template<typename FBLineWriter>
static void writeFramebufferLW(u32 width, u32 height, const u8 *data, u32 dstAddr, FB_W_CTRL_type fb_w_ctrl, u32 linestride, const Rect& clip)
{
int bpp = FBLineWriter::BytesPerPixel;
u32 padding = linestride;
if (padding > width * bpp)
padding = padding - width * bpp;
else
padding = 0;
const u8 *p = data + 4 * clip.origin.y * width;
dstAddr += bpp * clip.origin.y * (width + padding / bpp);
const u32 clipWidth = std::min<u32>(width, clip.origin.x + clip.size.x);
height = std::min<u32>(height, clip.origin.y + clip.size.y);
FBPixelWriter pixWriter(dstAddr);
FBLineWriter lineWriter(fb_w_ctrl, pixWriter);
const unsigned dp1 = 4 * clip.origin.x;
const unsigned adv1 = bpp * clip.origin.x;
const unsigned adv2 = padding + (width - clipWidth) * bpp;
const unsigned dp2 = (width - clipWidth) * 4;
for (u32 l = clip.origin.y; l < height; l++)
{
p += dp1;
pixWriter.advance(adv1);
lineWriter.write(clip.origin.x, clipWidth, p);
pixWriter.advance(adv2);
p += dp2;
}
}
#if HOST_CPU == CPU_ARM64
template<int PackMode, int Red, int Green, int Blue, int Alpha>
static inline u16 packFramebuffer16Pixel(const u8 *pixel, FB_W_CTRL_type fb_w_ctrl)
{
const u16 red = pixel[Red];
const u16 green = pixel[Green];
const u16 blue = pixel[Blue];
if constexpr (PackMode == 0)
return ((red >> 3) << 10) | ((green >> 3) << 5) | (blue >> 3)
| ((fb_w_ctrl.fb_kval & 0x80) << 8);
else if constexpr (PackMode == 1)
return ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
else if constexpr (PackMode == 2)
return ((pixel[Alpha] >> 4) << 12) | ((red >> 4) << 8)
| ((green >> 4) << 4) | (blue >> 4);
else
return (pixel[Alpha] >= fb_w_ctrl.fb_alpha_threshold ? 0x8000 : 0)
| ((red >> 3) << 10) | ((green >> 3) << 5) | (blue >> 3);
}
template<int PackMode>
static inline uint16x8_t packFramebuffer16Pixels(uint8x8_t red, uint8x8_t green,
uint8x8_t blue, uint8x8_t alpha, FB_W_CTRL_type fb_w_ctrl)
{
const uint16x8_t red16 = vmovl_u8(red);
const uint16x8_t green16 = vmovl_u8(green);
const uint16x8_t blue16 = vmovl_u8(blue);
if constexpr (PackMode == 0)
{
uint16x8_t packed = vshlq_n_u16(vshrq_n_u16(red16, 3), 10);
packed = vorrq_u16(packed, vshlq_n_u16(vshrq_n_u16(green16, 3), 5));
packed = vorrq_u16(packed, vshrq_n_u16(blue16, 3));
return vorrq_u16(packed, vdupq_n_u16((fb_w_ctrl.fb_kval & 0x80) << 8));
}
else if constexpr (PackMode == 1)
{
uint16x8_t packed = vshlq_n_u16(vshrq_n_u16(red16, 3), 11);
packed = vorrq_u16(packed, vshlq_n_u16(vshrq_n_u16(green16, 2), 5));
return vorrq_u16(packed, vshrq_n_u16(blue16, 3));
}
else if constexpr (PackMode == 2)
{
uint16x8_t packed = vshlq_n_u16(vshrq_n_u16(vmovl_u8(alpha), 4), 12);
packed = vorrq_u16(packed, vshlq_n_u16(vshrq_n_u16(red16, 4), 8));
packed = vorrq_u16(packed, vshlq_n_u16(vshrq_n_u16(green16, 4), 4));
return vorrq_u16(packed, vshrq_n_u16(blue16, 4));
}
else
{
uint16x8_t packed = vshlq_n_u16(vshrq_n_u16(red16, 3), 10);
packed = vorrq_u16(packed, vshlq_n_u16(vshrq_n_u16(green16, 3), 5));
packed = vorrq_u16(packed, vshrq_n_u16(blue16, 3));
const uint16x8_t alphaBit = vandq_u16(
vshlq_n_u16(vmovl_u8(vcge_u8(alpha, vdup_n_u8(fb_w_ctrl.fb_alpha_threshold))), 8),
vdupq_n_u16(0x8000));
return vorrq_u16(packed, alphaBit);
}
}
// 8 words to 8 interleaved 32-bit VRAM locations
static inline void storeFramebufferWords(u8 *mapped, uint32x4_t words0, uint32x4_t words1)
{
vst1q_lane_u32((u32 *)(mapped + 0), words0, 0);
vst1q_lane_u32((u32 *)(mapped + 8), words0, 1);
vst1q_lane_u32((u32 *)(mapped + 16), words0, 2);
vst1q_lane_u32((u32 *)(mapped + 24), words0, 3);
vst1q_lane_u32((u32 *)(mapped + 32), words1, 0);
vst1q_lane_u32((u32 *)(mapped + 40), words1, 1);
vst1q_lane_u32((u32 *)(mapped + 48), words1, 2);
vst1q_lane_u32((u32 *)(mapped + 56), words1, 3);
}
static inline void storeFramebuffer16Pixels(u8 *mapped, uint16x8_t pixels0, uint16x8_t pixels1)
{
storeFramebufferWords(mapped, vreinterpretq_u32_u16(pixels0), vreinterpretq_u32_u16(pixels1));
}
template<int PackMode, int Red, int Green, int Blue, int Alpha>
static void writeFramebuffer16Neon(u32 width, u32 height, const u8 *data, u32 dstAddr,
FB_W_CTRL_type fb_w_ctrl, u32 linestride, const Rect& clip)
{
u32 padding = linestride;
if (padding > width * 2)
padding -= width * 2;
else
padding = 0;
const int clipWidth = std::min<int>(width, clip.origin.x + clip.size.x);
height = std::min<u32>(height, clip.origin.y + clip.size.y);
if (clip.origin.x >= clipWidth || clip.origin.y >= (int)height)
return;
const u32 lineStride = width * 2 + padding;
const int count = clipWidth - clip.origin.x;
for (u32 y = clip.origin.y; y < height; y++)
{
const u8 *pixel = data + ((size_t)y * width + clip.origin.x) * 4;
u32 addr = dstAddr + y * lineStride + clip.origin.x * 2;
int x = 0;
for (; x < count && (addr & 3) != 0; x++, addr += 2, pixel += 4)
pvr_write32p<u16, true>(addr, packFramebuffer16Pixel<PackMode, Red, Green, Blue, Alpha>(pixel, fb_w_ctrl));
for (; x + 16 <= count && pvr32SpanIsContiguous(addr, 32); x += 16, addr += 32, pixel += 64)
{
const uint8x16x4_t rgba = vld4q_u8(pixel);
const uint16x8_t packed0 = packFramebuffer16Pixels<PackMode>(
vget_low_u8(rgba.val[Red]), vget_low_u8(rgba.val[Green]),
vget_low_u8(rgba.val[Blue]), vget_low_u8(rgba.val[Alpha]), fb_w_ctrl);
const uint16x8_t packed1 = packFramebuffer16Pixels<PackMode>(
vget_high_u8(rgba.val[Red]), vget_high_u8(rgba.val[Green]),
vget_high_u8(rgba.val[Blue]), vget_high_u8(rgba.val[Alpha]), fb_w_ctrl);
storeFramebuffer16Pixels(&vram[pvr_map32(addr)], packed0, packed1);
}
for (; x < count; x++, addr += 2, pixel += 4)
pvr_write32p<u16, true>(addr, packFramebuffer16Pixel<PackMode, Red, Green, Blue, Alpha>(pixel, fb_w_ctrl));
}
}
// 0888 KRGB and 8888 ARGB: one pixel is one VRAM word, so 8 pixels fill the
// same 8 interleaved words as 16 16-bit pixels.
// 888 (24-bit packed) stays scalar: its pixels straddle VRAM words.
template<int PackMode, int Red, int Green, int Blue, int Alpha>
static inline u32 packFramebuffer32Pixel(const u8 *pixel, u32 kval)
{
const u32 high = PackMode == 5 ? kval : ((u32)pixel[Alpha] << 24);
return ((u32)pixel[Red] << 16) | ((u32)pixel[Green] << 8) | pixel[Blue] | high;
}
template<int PackMode>
static inline void packFramebuffer32Pixels(uint8x8_t red, uint8x8_t green, uint8x8_t blue,
uint8x8_t alpha, u32 kval, uint32x4_t& words0, uint32x4_t& words1)
{
// word = B | G << 8 | R << 16 | (K or A) << 24, built as two 16-bit halves
const uint16x8_t low = vorrq_u16(vmovl_u8(blue), vshlq_n_u16(vmovl_u8(green), 8));
uint16x8_t high;
if constexpr (PackMode == 5)
high = vorrq_u16(vmovl_u8(red), vdupq_n_u16((u16)(kval >> 16)));
else
high = vorrq_u16(vmovl_u8(red), vshlq_n_u16(vmovl_u8(alpha), 8));
words0 = vorrq_u32(vmovl_u16(vget_low_u16(low)), vshlq_n_u32(vmovl_u16(vget_low_u16(high)), 16));
words1 = vorrq_u32(vmovl_u16(vget_high_u16(low)), vshlq_n_u32(vmovl_u16(vget_high_u16(high)), 16));
}
// Same address walk as writeFramebufferLW(), so the output is identical to the
// scalar path, including when padding isn't a multiple of 4.
template<int PackMode, int Red, int Green, int Blue, int Alpha>
static void writeFramebuffer32Neon(u32 width, u32 height, const u8 *data, u32 dstAddr,
FB_W_CTRL_type fb_w_ctrl, u32 linestride, const Rect& clip)
{
constexpr u32 bpp = 4;
u32 padding = linestride;
if (padding > width * bpp)
padding -= width * bpp;
else
padding = 0;
const u32 kval = fb_w_ctrl.fb_kval << 24;
const u8 *p = data + 4 * clip.origin.y * width;
u32 addr = dstAddr + bpp * clip.origin.y * (width + padding / bpp);
const u32 clipWidth = std::min<u32>(width, clip.origin.x + clip.size.x);
height = std::min<u32>(height, clip.origin.y + clip.size.y);
const int count = clipWidth > (u32)clip.origin.x ? clipWidth - clip.origin.x : 0;
for (u32 l = clip.origin.y; l < height; l++)
{
p += 4 * clip.origin.x;
addr += bpp * clip.origin.x;
int x = 0;
for (; x + 8 <= count && pvr32SpanIsContiguous(addr, 32); x += 8, addr += 32, p += 32)
{
const uint8x8x4_t rgba = vld4_u8(p);
uint32x4_t words0, words1;
packFramebuffer32Pixels<PackMode>(rgba.val[Red], rgba.val[Green], rgba.val[Blue],
rgba.val[Alpha], kval, words0, words1);
storeFramebufferWords(&vram[pvr_map32(addr)], words0, words1);
}
for (; x < count; x++, addr += bpp, p += 4)
pvr_write32p<u32, true>(addr, packFramebuffer32Pixel<PackMode, Red, Green, Blue, Alpha>(p, kval));
addr += padding + (width - clipWidth) * bpp;
p += (width - clipWidth) * 4;
}
}
#endif
template<int Red, int Green, int Blue, int Alpha>
void WriteFramebuffer(u32 width, u32 height, const u8 *data, u32 dstAddr, FB_W_CTRL_type fb_w_ctrl, u32 linestride, const Rect& clip)
{
switch (fb_w_ctrl.fb_packmode)
{
case 0: // 0555 KRGB 16 bit
#if HOST_CPU == CPU_ARM64
writeFramebuffer16Neon<0, Red, Green, Blue, Alpha>(width, height, data, dstAddr, fb_w_ctrl, linestride, clip);
#else
writeFramebufferLW<FBLineWriter0555<Red, Green, Blue, Alpha, FBPixelWriter>>(width, height, data, dstAddr, fb_w_ctrl, linestride, clip);
#endif
break;
case 1: // 565 RGB 16 bit
#if HOST_CPU == CPU_ARM64
writeFramebuffer16Neon<1, Red, Green, Blue, Alpha>(width, height, data, dstAddr, fb_w_ctrl, linestride, clip);
#else
writeFramebufferLW<FBLineWriter565<Red, Green, Blue, Alpha, FBPixelWriter>>(width, height, data, dstAddr, fb_w_ctrl, linestride, clip);
#endif
break;
case 2: // 4444 ARGB 16 bit
#if HOST_CPU == CPU_ARM64
writeFramebuffer16Neon<2, Red, Green, Blue, Alpha>(width, height, data, dstAddr, fb_w_ctrl, linestride, clip);
#else
writeFramebufferLW<FBLineWriter4444<Red, Green, Blue, Alpha, FBPixelWriter>>(width, height, data, dstAddr, fb_w_ctrl, linestride, clip);
#endif
break;
case 3: // 1555 ARGB 16 bit
#if HOST_CPU == CPU_ARM64
writeFramebuffer16Neon<3, Red, Green, Blue, Alpha>(width, height, data, dstAddr, fb_w_ctrl, linestride, clip);
#else
writeFramebufferLW<FBLineWriter1555<Red, Green, Blue, Alpha, FBPixelWriter>>(width, height, data, dstAddr, fb_w_ctrl, linestride, clip);
#endif
break;
case 4: // 888 RGB 24 bit packed
writeFramebufferLW<FBLineWriter888<Red, Green, Blue, Alpha, FBPixelWriter>>(width, height, data, dstAddr, fb_w_ctrl, linestride, clip);
break;
case 5: // 0888 KRGB 32 bit
#if HOST_CPU == CPU_ARM64
writeFramebuffer32Neon<5, Red, Green, Blue, Alpha>(width, height, data, dstAddr, fb_w_ctrl, linestride, clip);
#else
writeFramebufferLW<FBLineWriter0888<Red, Green, Blue, Alpha, FBPixelWriter>>(width, height, data, dstAddr, fb_w_ctrl, linestride, clip);
#endif
break;
case 6: // 8888 ARGB 32 bit
#if HOST_CPU == CPU_ARM64
writeFramebuffer32Neon<6, Red, Green, Blue, Alpha>(width, height, data, dstAddr, fb_w_ctrl, linestride, clip);
#else
writeFramebufferLW<FBLineWriter8888<Red, Green, Blue, Alpha, FBPixelWriter>>(width, height, data, dstAddr, fb_w_ctrl, linestride, clip);
#endif
break;
default:
ERROR_LOG(PVR, "Invalid framebuffer format: %d", fb_w_ctrl.fb_packmode);
break;
}
}
template void WriteFramebuffer<0, 1, 2, 3>(u32 width, u32 height, const u8 *data, u32 dstAddr, FB_W_CTRL_type fb_w_ctrl,
u32 linestride, const Rect& clip);
template void WriteFramebuffer<2, 1, 0, 3>(u32 width, u32 height, const u8 *data, u32 dstAddr, FB_W_CTRL_type fb_w_ctrl,
u32 linestride, const Rect& clip);
void BaseTextureCacheData::invalidate()
{
dirty = FrameCount;
libCore_vramlock_Unlock_block_wb(lock_block);
lock_block = nullptr;
}
#ifdef TEST_AUTOMATION
#include <stb_image_write.h>
void dump_screenshot(u8 *buffer, u32 width, u32 height, bool alpha, u32 rowPitch, bool invertY)
{
stbi_flip_vertically_on_write((int)invertY);
stbi_write_png("screenshot.png", width, height, alpha ? 4 : 3, buffer, rowPitch);
}
#endif