mirror of
https://github.com/libretro/parallel-dc.git
synced 2026-11-10 17:26:21 -10:00
The loader thread worked out the hash a texture's replacement is filed under itself: from video memory, which with threaded rendering the game is writing again by then, and from the texture's own fields, which the renderer may be updating for the next request. It also looked at the texture's "dirty" stamp, a plain variable of the renderer's. The renderer does the hashing now, when it asks - it is updating the texture at that moment, and the game is held while it does - and leaves the three names for the loader in the texture before it counts the request. The loader reads the count and then the names, loads, and takes its count off: if a request came in meanwhile, the count it took off is not all there was, it goes round again, and what it loaded from names that may have been half one request's and half the next's is dropped. The image and its size are still only looked at by the renderer once the count is back to zero. Nothing else of a texture is read by the loader. (The "dirty" check saved loading for a texture that had been invalidated since it asked. Such a texture asks again when it is next updated, and the image loaded for nothing is freed then, or with the texture.) tools/threads: the model of the loader's queue carries a three-word request the same way and checks that what was made of each node's last request is that request's, whole; under TSan and ASan + UBSan. Run: the test disc with a pack of sixteen magenta replacements for the textures it dumps, threaded rendering on - the screenshot has 41,148 magenta pixels against 24,228 without the pack; with the option on and no pack, 24,228. Not done: the audit's bound on what is queued. A texture is on the list at most once and holds at most one loaded image, so both are bounded by the number of textures, not by bytes.
1097 lines
32 KiB
C++
1097 lines
32 KiB
C++
/* The texture cache works for the renderer: it reads the registers the
|
|
* frame is rendered with, not the ones the emulation has moved on to. */
|
|
#define PVR_REGS_FOR_RENDERER
|
|
#include "TexCache.h"
|
|
#include "lockfree.h"
|
|
#include "CustomTexture.h"
|
|
#ifdef HAVE_TEXUPSCALE
|
|
#include "deps/xbrz/xbrz.h"
|
|
#endif
|
|
#include "hw/pvr/pvr_mem.h"
|
|
#include "hw/mem/_vmem.h"
|
|
#include "hw/mem/vmem32.h"
|
|
#include "hw/sh4/modules/mmu.h"
|
|
|
|
#include <algorithm>
|
|
// for a hash state that can be a local: ComputeHash() allocates nothing
|
|
#define XXH_STATIC_LINKING_ONLY
|
|
#include <deps/xxhash/xxhash.h>
|
|
|
|
#if defined(HAVE_TEXUPSCALE) && !defined(TARGET_NO_OPENMP)
|
|
#include <omp.h>
|
|
#endif
|
|
|
|
u8* vq_codebook;
|
|
u32 palette_index;
|
|
bool KillTex=false;
|
|
u32 palette16_ram[1024];
|
|
u32 palette32_ram[1024];
|
|
u32 pal_hash_256[4];
|
|
u32 pal_hash_16[64];
|
|
bool palette_updated;
|
|
|
|
// 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 void rend_text_invl(vram_block* bl);
|
|
|
|
u32 detwiddle[2][11][1024];
|
|
//input : address in the yyyyyxxxxx format
|
|
//output : address in the xyxyxyxy format
|
|
//U : x resolution , V : y resolution
|
|
//twiddle works on 64b words
|
|
|
|
|
|
static u32 twiddle_slow(u32 x,u32 y,u32 x_sz,u32 y_sz)
|
|
{
|
|
u32 rv=0;//low 2 bits are directly passed -> needs some misc stuff to work.However
|
|
//Pvr internally maps the 64b banks "as if" they were twiddled :p
|
|
|
|
u32 sh=0;
|
|
x_sz>>=1;
|
|
y_sz>>=1;
|
|
while(x_sz!=0 || y_sz!=0)
|
|
{
|
|
if (y_sz)
|
|
{
|
|
u32 temp=y&1;
|
|
rv|=temp<<sh;
|
|
|
|
y_sz>>=1;
|
|
y>>=1;
|
|
sh++;
|
|
}
|
|
if (x_sz)
|
|
{
|
|
u32 temp=x&1;
|
|
rv|=temp<<sh;
|
|
|
|
x_sz>>=1;
|
|
x>>=1;
|
|
sh++;
|
|
}
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
static void BuildTwiddleTables()
|
|
{
|
|
for (u32 s = 0; s < 11; s++)
|
|
{
|
|
u32 x_sz = 1024;
|
|
u32 y_sz = 1 << s;
|
|
for (u32 i = 0; i < x_sz; i++)
|
|
{
|
|
detwiddle[0][s][i] = twiddle_slow(i, 0, x_sz, y_sz);
|
|
detwiddle[1][s][i] = twiddle_slow(0, i, y_sz, x_sz);
|
|
}
|
|
}
|
|
}
|
|
|
|
static OnLoad btt(&BuildTwiddleTables);
|
|
|
|
/* Converts the palette for the renderer when it is not the one converted
|
|
* last. Called by the renderer for each frame, with the registers that
|
|
* frame is drawn with.
|
|
*
|
|
* This used to be driven by a flag the register writes set, and was run
|
|
* by the emulation thread when the game started a render. With Threaded
|
|
* Rendering the frame before could still be drawing then, reading the very
|
|
* tables this rewrites. Now the tables belong to the thread that draws, and
|
|
* whether they are stale is seen from the palette itself: 4 KB to compare. */
|
|
void palette_update()
|
|
{
|
|
static u32 converted[1024];
|
|
static u32 converted_ctrl = 0xFFFFFFFF;
|
|
const u32 ctrl = PAL_RAM_CTRL & 3;
|
|
|
|
if (ctrl == converted_ctrl && !memcmp(converted, PALETTE_RAM, sizeof(converted)))
|
|
return;
|
|
converted_ctrl = ctrl;
|
|
memcpy(converted, PALETTE_RAM, sizeof(converted));
|
|
palette_updated = true;
|
|
switch (ctrl)
|
|
{
|
|
case 0:
|
|
for (int i=0;i<1024;i++)
|
|
{
|
|
palette16_ram[i] = ARGB1555(PALETTE_RAM[i]);
|
|
palette32_ram[i] = ARGB1555_32(PALETTE_RAM[i]);
|
|
}
|
|
break;
|
|
|
|
case 1:
|
|
for (int i=0;i<1024;i++)
|
|
{
|
|
palette16_ram[i] = ARGB565(PALETTE_RAM[i]);
|
|
palette32_ram[i] = ARGB565_32(PALETTE_RAM[i]);
|
|
}
|
|
break;
|
|
|
|
case 2:
|
|
for (int i=0;i<1024;i++)
|
|
{
|
|
palette16_ram[i] = ARGB4444(PALETTE_RAM[i]);
|
|
palette32_ram[i] = ARGB4444_32(PALETTE_RAM[i]);
|
|
}
|
|
break;
|
|
|
|
case 3:
|
|
for (int i=0;i<1024;i++)
|
|
{
|
|
palette16_ram[i] = ARGB8888(PALETTE_RAM[i]);
|
|
palette32_ram[i] = ARGB8888_32(PALETTE_RAM[i]);
|
|
}
|
|
break;
|
|
}
|
|
for (int i = 0; i < 64; i++)
|
|
pal_hash_16[i] = XXH32(&PALETTE_RAM[i << 4], 16 * 4, 7);
|
|
for (int i = 0; i < 4; i++)
|
|
pal_hash_256[i] = XXH32(&PALETTE_RAM[i << 8], 256 * 4, 7);
|
|
}
|
|
|
|
static std::vector<vram_block*> VramLocks[VRAM_SIZE_MAX / PAGE_SIZE];
|
|
VArray2 vram; // vram 32-64b
|
|
|
|
//List functions
|
|
//
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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; }))
|
|
_vmem_protect_vram(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);
|
|
}
|
|
}
|
|
|
|
/* Writes to protected video memory, by page.
|
|
*
|
|
* The lock lists above belong to the render thread; nothing else reads or
|
|
* changes them. The emulation thread finds out that a protected page was
|
|
* written when the write faults, possibly inside a signal handler, and all
|
|
* it does there is open the page up again and mark it here. The render
|
|
* thread collects the marks before it looks at any texture and drops every
|
|
* lock on those pages, which is what marks their textures as changed.
|
|
*
|
|
* The emulation thread unprotects first and marks second. A page it has
|
|
* opened up is therefore always marked afterwards, so the render thread
|
|
* can go on treating "a page with locks on it is protected" as true: when
|
|
* it is not, a mark is on its way that takes those locks off again. */
|
|
static cMarkSet<VRAM_SIZE_MAX / PAGE_SIZE> vram_writes;
|
|
|
|
static void libCore_vramlock_Unlock_block_wb(vram_block* block);
|
|
|
|
/* Render thread: apply the writes the emulation thread has marked. */
|
|
static void vramlock_collect()
|
|
{
|
|
vram_writes.Collect([](unsigned page) {
|
|
std::vector<vram_block *>& list = VramLocks[page];
|
|
|
|
for (auto& lock : list)
|
|
{
|
|
if (lock != nullptr)
|
|
{
|
|
rend_text_invl(lock);
|
|
|
|
if (lock != nullptr)
|
|
{
|
|
ERROR_LOG(PVR, "Error : pvr is supposed to remove lock");
|
|
die("Invalid state");
|
|
}
|
|
}
|
|
}
|
|
list.clear();
|
|
});
|
|
}
|
|
|
|
void libCore_vramlock_Lock(u32 start_offset64, u32 end_offset64, BaseTextureCacheData *texture)
|
|
{
|
|
if (end_offset64>(VRAM_SIZE-1))
|
|
{
|
|
WARN_LOG(PVR, "vramlock_Lock_64: end_offset64>(VRAM_SIZE-1) \n Tried to lock area out of vram , possibly bug on the pvr plugin");
|
|
end_offset64=(VRAM_SIZE-1);
|
|
}
|
|
|
|
if (start_offset64>end_offset64)
|
|
{
|
|
WARN_LOG(PVR, "vramlock_Lock_64: start_offset64>end_offset64 \n Tried to lock negative block , possibly bug on the pvr plugin");
|
|
start_offset64=0;
|
|
}
|
|
|
|
/* Writes that came before this lock are not its business. */
|
|
vramlock_collect();
|
|
|
|
if (texture->lock_block == nullptr)
|
|
{
|
|
vram_block* block=(vram_block* )malloc(sizeof(vram_block));
|
|
|
|
block->end=end_offset64;
|
|
block->start=start_offset64;
|
|
block->len=end_offset64-start_offset64+1;
|
|
block->userdata = texture;
|
|
block->type=64;
|
|
|
|
// This also protects vram if needed
|
|
vramlock_list_add(block);
|
|
texture->lock_block = block;
|
|
}
|
|
}
|
|
|
|
/* A write to video memory hit a protected page. Called on the emulation
|
|
* thread from the fault handler, so it stays within what a signal handler
|
|
* may do: system calls and atomics, no locks and no allocation. The render
|
|
* thread calls it too, before it writes to video memory itself. */
|
|
bool VramLockedWriteOffset(size_t offset)
|
|
{
|
|
if (offset >= VRAM_SIZE)
|
|
return false;
|
|
|
|
_vmem_unprotect_vram((u32)(offset & ~PAGE_MASK), PAGE_SIZE);
|
|
vram_writes.Mark((unsigned)(offset / PAGE_SIZE));
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VramLockedWrite(u8* address)
|
|
{
|
|
u32 offset = _vmem_get_vram_offset(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);
|
|
free(block);
|
|
}
|
|
|
|
#ifdef HAVE_TEXUPSCALE
|
|
#ifndef TARGET_NO_OPENMP
|
|
static inline int getThreadCount()
|
|
{
|
|
int tcount = omp_get_num_procs() - 1;
|
|
if (tcount < 1)
|
|
tcount = 1;
|
|
return std::min(tcount, 3);
|
|
}
|
|
|
|
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)
|
|
{
|
|
#ifndef TARGET_NO_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
|
|
}
|
|
|
|
#endif
|
|
|
|
struct PvrTexInfo
|
|
{
|
|
const char* name;
|
|
int bpp; //4/8 for pal. 16 for yuv, rgb, argb
|
|
TextureType type;
|
|
// Conversion to 16 bpp
|
|
TexConvFP *PL;
|
|
TexConvFP *TW;
|
|
TexConvFP *VQ;
|
|
// Conversion to 32 bpp
|
|
TexConvFP32 *PL32;
|
|
TexConvFP32 *TW32;
|
|
TexConvFP32 *VQ32;
|
|
// Conversion to 8 bpp (palette)
|
|
TexConvFP8 *TW8;
|
|
// Planar and compressed, to 32 bpp
|
|
TexConvFP32 *PLVQ32;
|
|
};
|
|
|
|
static const PvrTexInfo format[8] =
|
|
{ // name bpp Final format Planar Twiddled VQ Planar(32b) Twiddled(32b) VQ (32b) Palette (8b)
|
|
{"1555", 16, TextureType::_5551, tex1555_PL, tex1555_TW, tex1555_VQ, tex1555_PL32, tex1555_TW32, tex1555_VQ32, nullptr, tex1555_PLVQ32 }, //1555
|
|
{"565", 16, TextureType::_565, tex565_PL, tex565_TW, tex565_VQ, tex565_PL32, tex565_TW32, tex565_VQ32, nullptr, tex565_PLVQ32 }, //565
|
|
{"4444", 16, TextureType::_4444, tex4444_PL, tex4444_TW, tex4444_VQ, tex4444_PL32, tex4444_TW32, tex4444_VQ32, nullptr, tex4444_PLVQ32 }, //4444
|
|
{"yuv", 16, TextureType::_8888, nullptr, nullptr, nullptr, texYUV422_PL, texYUV422_TW, texYUV422_VQ, nullptr, texYUV422_PLVQ }, //yuv
|
|
{"bumpmap", 16, TextureType::_4444, texBMP_PL, texBMP_TW, texBMP_VQ, tex4444_PL32, tex4444_TW32, tex4444_VQ32, nullptr, tex4444_PLVQ32 }, //bump map
|
|
{"pal4", 4, TextureType::_5551, nullptr, texPAL4_TW, texPAL4_VQ, nullptr, texPAL4_TW32, texPAL4_VQ32, texPAL4PT_TW }, //pal4
|
|
{"pal8", 8, TextureType::_5551, nullptr, texPAL8_TW, texPAL8_VQ, nullptr, texPAL8_TW32, texPAL8_VQ32, texPAL8PT_TW }, //pal8
|
|
{"ns/1555", 0}, // Not supported (1555)
|
|
};
|
|
|
|
static const u32 VQMipPoint[11] =
|
|
{
|
|
0x00000,//1
|
|
0x00001,//2
|
|
0x00002,//4
|
|
0x00006,//8
|
|
0x00016,//16
|
|
0x00056,//32
|
|
0x00156,//64
|
|
0x00556,//128
|
|
0x01556,//256
|
|
0x05556,//512
|
|
0x15556//1024
|
|
};
|
|
static 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()
|
|
{
|
|
char str[512];
|
|
sprintf(str, "Texture: %s", GetPixelFormatName());
|
|
|
|
if (tcw.VQ_Comp)
|
|
strcat(str, " VQ");
|
|
else if (tcw.ScanOrder == 0)
|
|
strcat(str, " TW");
|
|
else if (tcw.StrideSel)
|
|
strcat(str, " Stride");
|
|
|
|
if (tcw.ScanOrder == 0 && tcw.MipMapped)
|
|
strcat(str, " MM");
|
|
if (tsp.FilterMode != 0)
|
|
strcat(str, " Bilinear");
|
|
|
|
sprintf(str + strlen(str), " %dx%d @ 0x%X", 8 << tsp.TexU, 8 << tsp.TexV, tcw.TexAddr << 3);
|
|
std::string id = GetId();
|
|
sprintf(str + strlen(str), " id=%s", id.c_str());
|
|
DEBUG_LOG(RENDERER, "%s", str);
|
|
}
|
|
|
|
/* Hash of the vram content backing this texture, over the same range the
|
|
* write-protection lock would cover, clamped to VRAM like the lock is. Used to
|
|
* detect changes for frequently-updated textures that run unprotected. */
|
|
u32 BaseTextureCacheData::ComputeVramHash()
|
|
{
|
|
u32 start = sa_tex & VRAM_MASK;
|
|
u32 end = sa + size;
|
|
if (end > VRAM_SIZE)
|
|
end = VRAM_SIZE;
|
|
if (end <= start)
|
|
return 0;
|
|
return XXH32(&vram[start], end - start, 7);
|
|
}
|
|
|
|
//true if : dirty or paletted texture and hashes don't match
|
|
bool BaseTextureCacheData::NeedsUpdate() {
|
|
/* Take in what the emulation thread has written since the last look. */
|
|
if (vram_writes.Pending())
|
|
vramlock_collect();
|
|
|
|
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;
|
|
}
|
|
/* Frequently-updated texture running unprotected (see Update): compare the
|
|
* content hash of the vram range instead of relying on a write fault. */
|
|
if (!rc && lock_block == nullptr && Updates >= FREQUENT_UPDATE_COUNT
|
|
&& ComputeVramHash() != vram_hash)
|
|
rc = true;
|
|
|
|
return rc;
|
|
}
|
|
|
|
bool BaseTextureCacheData::Delete()
|
|
{
|
|
if (retro_atomic_load_acquire_int(&custom_load_in_progress) > 0)
|
|
return false;
|
|
|
|
if (lock_block)
|
|
libCore_vramlock_Unlock_block_wb(lock_block);
|
|
lock_block = nullptr;
|
|
|
|
free(custom_image_data);
|
|
|
|
return true;
|
|
}
|
|
|
|
void BaseTextureCacheData::Create()
|
|
{
|
|
//Reset state info ..
|
|
Updates = 0;
|
|
dirty = FrameCount;
|
|
lock_block = nullptr;
|
|
custom_image_data = nullptr;
|
|
retro_atomic_store_release_int(&custom_load_in_progress, 0);
|
|
retro_atomic_int_init(&custom_request_hash, 0);
|
|
retro_atomic_int_init(&custom_request_old_hash, 0);
|
|
retro_atomic_int_init(&custom_request_old_vqhash, 0);
|
|
|
|
//decode info from tsp/tcw into the texture struct
|
|
tex = &format[tcw.PixelFmt == PixelReserved ? Pixel1555 : tcw.PixelFmt]; //texture format table entry
|
|
|
|
sa_tex = (tcw.TexAddr << 3) & VRAM_MASK; //texture start address
|
|
sa = sa_tex; //data texture start address (modified for MIPs, as needed)
|
|
w = 8 << tsp.TexU; //tex width
|
|
h = 8 << tsp.TexV; //tex height
|
|
|
|
//PAL texture
|
|
if (tex->bpp == 4)
|
|
palette_index = tcw.PalSelect << 4;
|
|
else if (tex->bpp == 8)
|
|
palette_index = (tcw.PalSelect >> 4) << 8;
|
|
|
|
texconv8 = nullptr;
|
|
|
|
/* Compressed and mipmapped, a texture is twiddled whatever this bit
|
|
* says: Star Wars Demolition sets it, and its textures came out
|
|
* corrupted (upstream: or took the emulator down). Not for a paletted
|
|
* texture, where the bit is part of the palette selection. */
|
|
if (tcw.VQ_Comp == 1 && tcw.MipMapped == 1 && !IsPaletted())
|
|
tcw.ScanOrder = 0;
|
|
|
|
if (tcw.ScanOrder && (tex->PL || tex->PL32))
|
|
{
|
|
//Texture is stored 'planar' in memory, no deswizzle is needed
|
|
if (tcw.MipMapped != 0)
|
|
{
|
|
WARN_LOG(RENDERER, "Warning: planar texture with mipmaps (invalid)");
|
|
tcw.MipMapped = 0;
|
|
}
|
|
|
|
//Planar textures support stride selection, mostly used for non power of 2 textures (videos)
|
|
int stride = w;
|
|
if (tcw.StrideSel)
|
|
{
|
|
stride = (TEXT_CONTROL & 31) * 32;
|
|
// a stride of nothing is no stride: the texture's own width
|
|
if (stride == 0)
|
|
stride = w;
|
|
}
|
|
|
|
if (tcw.VQ_Comp != 0 && tex->PLVQ32 != nullptr)
|
|
{
|
|
/* Compressed as well. This was taken for a mistake and the
|
|
* compression ignored; it is a texture like any other (the
|
|
* Genesis emulator in the Teenage Mutant Ninja Turtles
|
|
* collection draws its screen with one). */
|
|
vq_codebook = sa;
|
|
texconv = nullptr;
|
|
texconv32 = tex->PLVQ32;
|
|
size = 256 * 4 * 2 + stride * h / 4;
|
|
}
|
|
else
|
|
{
|
|
tcw.VQ_Comp = 0;
|
|
//Call the format specific conversion code
|
|
texconv = tex->PL;
|
|
texconv32 = tex->PL32;
|
|
//calculate the size, in bytes, for the locking
|
|
size = stride * h * tex->bpp / 8;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* These two bits mean nothing for a twiddled texture and are taken
|
|
* out of the way - unless the texture is paletted. There they are
|
|
* the top two bits of the palette selection, which says which part
|
|
* of the palette the texture uses. With them cleared, NeedsUpdate()
|
|
* looked for changes in the wrong part: a texture using the upper
|
|
* half of the palette was not decoded again when its colours
|
|
* changed, and was when some other texture's did. */
|
|
if (!IsPaletted())
|
|
{
|
|
tcw.ScanOrder = 0;
|
|
tcw.StrideSel = 0;
|
|
}
|
|
// Quake 3 Arena uses one
|
|
if (tcw.MipMapped)
|
|
// Mipmapped texture must be square and TexV is ignored
|
|
h = w;
|
|
|
|
if (tcw.VQ_Comp)
|
|
{
|
|
verify(tex->VQ != NULL || tex->VQ32 != NULL);
|
|
vq_codebook = sa;
|
|
if (tcw.MipMapped)
|
|
sa += VQMipPoint[tsp.TexU + 3];
|
|
texconv = tex->VQ;
|
|
texconv32 = tex->VQ32;
|
|
/* From sa: the codebook the converters skip, and a byte for
|
|
* every four pixels. This was half the pixels' share and no
|
|
* codebook, so a write to the rest of the texture was not
|
|
* watched for, and the texture stayed as it was. */
|
|
size = 256 * 4 * 2 + w * h / 4;
|
|
}
|
|
else
|
|
{
|
|
verify(tex->TW != NULL || tex->TW32 != NULL);
|
|
if (tcw.MipMapped)
|
|
sa += OtherMipPoint[tsp.TexU + 3] * tex->bpp / 8;
|
|
texconv = tex->TW;
|
|
texconv32 = tex->TW32;
|
|
size = w * h * tex->bpp / 8;
|
|
texconv8 = tex->TW8;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Where a texture is converted before it is handed to the GPU. One piece of
|
|
* memory, kept from one texture to the next and only ever grown: every
|
|
* update used to allocate a buffer the size of its texture and free it
|
|
* again a moment later, sixty megabytes a second of that for a video
|
|
* playing as a 512x512 texture. Whatever is in it is the last texture's and
|
|
* of no use, so growing it copies nothing. Updates happen on the one thread
|
|
* that renders. At most a 1024x1024 texture with its mipmaps, 5.6 MB. */
|
|
static void *tex_scratch;
|
|
static size_t tex_scratch_size;
|
|
|
|
static void *texcache_scratch(size_t size)
|
|
{
|
|
if (size > tex_scratch_size)
|
|
{
|
|
free(tex_scratch);
|
|
tex_scratch = malloc(size);
|
|
tex_scratch_size = tex_scratch != NULL ? size : 0;
|
|
}
|
|
return tex_scratch;
|
|
}
|
|
|
|
void texcache_scratch_free()
|
|
{
|
|
free(tex_scratch);
|
|
tex_scratch = NULL;
|
|
tex_scratch_size = 0;
|
|
}
|
|
|
|
void BaseTextureCacheData::ComputeHash()
|
|
{
|
|
// twiddled and compressed; for a paletted texture the scan order bit is part of the palette selection
|
|
if (tcw.VQ_Comp && !(tcw.ScanOrder && (tex->PL || tex->PL32)))
|
|
{
|
|
/* A compressed texture's name used to come from the first w*h/8
|
|
* bytes at sa - part of the codebook and none or some of the
|
|
* texture, so that two textures could share one. It is now the
|
|
* hash of the codebook and then of the whole texture, as upstream
|
|
* names them; the old name is kept, and custom textures saved
|
|
* under it are still found. */
|
|
XXH32_state_t state;
|
|
|
|
old_vqtexture_hash = XXH32(&vram[sa], w * h / 8, 7);
|
|
if (IsPaletted())
|
|
old_vqtexture_hash ^= palette_hash;
|
|
old_texture_hash = old_vqtexture_hash;
|
|
old_vqtexture_hash ^= tcw.full & 0xFC000000;
|
|
|
|
XXH32_reset(&state, 7);
|
|
XXH32_update(&state, &vram[sa_tex], 256 * 4 * 2);
|
|
XXH32_update(&state, &vram[sa + 256 * 4 * 2], w * h / 4);
|
|
texture_hash = XXH32_digest(&state);
|
|
if (IsPaletted())
|
|
texture_hash ^= palette_hash;
|
|
texture_hash ^= tcw.full & 0xFC000000;
|
|
return;
|
|
}
|
|
old_vqtexture_hash = 0;
|
|
texture_hash = XXH32(&vram[sa], size, 7);
|
|
if (IsPaletted())
|
|
texture_hash ^= palette_hash;
|
|
old_texture_hash = texture_hash;
|
|
texture_hash ^= tcw.full & 0xFC000000; // everything but texaddr, reserved and stride
|
|
}
|
|
|
|
void BaseTextureCacheData::Update()
|
|
{
|
|
//texture state tracking stuff
|
|
Updates++;
|
|
dirty=0;
|
|
|
|
tex_type = tex->type;
|
|
|
|
bool has_alpha = false;
|
|
if (IsPaletted())
|
|
{
|
|
if (IsGpuHandledPaletted(tsp, tcw, area))
|
|
tex_type = TextureType::_8;
|
|
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];
|
|
else
|
|
palette_hash = pal_hash_256[tcw.PalSelect >> 4];
|
|
}
|
|
|
|
::palette_index = this->palette_index; // might be used if pal. tex
|
|
::vq_codebook = &vram[vq_codebook]; // might be used if VQ tex
|
|
|
|
//texture conversion work
|
|
u32 stride = w;
|
|
|
|
if (tcw.StrideSel && tcw.ScanOrder && (tex->PL || tex->PL32))
|
|
{
|
|
stride = (TEXT_CONTROL & 31) * 32;
|
|
if (stride == 0)
|
|
stride = w;
|
|
}
|
|
|
|
/* How many of the texture's lines there are to read. All of them, but
|
|
* for a texture that runs past the end of video memory: Shenmue's Space
|
|
* Harrier mini-arcade loads one, and only uses the top of it.
|
|
*
|
|
* That was done by cutting the texture's height and size down for the
|
|
* update and putting the height back after. The size stayed cut - it is
|
|
* what is locked and hashed, and has to end inside video memory - so the
|
|
* second update found nothing running past the end, cut nothing, and
|
|
* read all the lines, the ones outside video memory included. And the
|
|
* first update converted into a buffer made for the cut height, then
|
|
* uploaded the whole. (From upstream, done here by what the texture
|
|
* would take whole rather than by the size as it stands.) */
|
|
u32 rows = h;
|
|
const bool cut_short = tcw.ScanOrder && stride > 0 && !tcw.VQ_Comp && tex->bpp != 0;
|
|
const u32 whole = cut_short ? stride * h * tex->bpp / 8 : size;
|
|
if (sa_tex > VRAM_SIZE || size == 0 || sa + (whole > size ? whole : size) > VRAM_SIZE)
|
|
{
|
|
if (sa < VRAM_SIZE && cut_short)
|
|
{
|
|
rows = (VRAM_SIZE - sa) * 8 / stride / tex->bpp;
|
|
size = stride * rows * tex->bpp/8;
|
|
}
|
|
else
|
|
{
|
|
WARN_LOG(RENDERER, "Warning: invalid texture. Address %08X %08X size %d", sa_tex, sa, size);
|
|
return;
|
|
}
|
|
}
|
|
if (settings.rend.CustomTextures)
|
|
custom_texture.LoadCustomTextureAsync(this);
|
|
|
|
void *temp_tex_buffer = NULL;
|
|
u32 upscaled_w = w;
|
|
u32 upscaled_h = h;
|
|
|
|
PixelBuffer<u16> pb16;
|
|
PixelBuffer<u32> pb32;
|
|
PixelBuffer<u8> pb8;
|
|
|
|
/* A planar texture is converted a row of 'stride' pixels at a time into
|
|
* rows of the texture's width. With a stride wider than the texture,
|
|
* every row runs on into the next, which is then written over it - and
|
|
* the last runs past the end of the buffer by the difference: room is
|
|
* left for that. With a narrower one the rest of each row was whatever
|
|
* the memory held, and went to the GPU so; it is cleared. */
|
|
const u32 stride_over = stride > w ? stride - w : 0;
|
|
|
|
// Figure out if we really need to use a 32-bit pixel buffer
|
|
bool textureUpscaling = settings.rend.TextureUpscale > 1
|
|
// Don't process textures that are too big
|
|
&& (int)(w * h) <= settings.rend.MaxFilteredTextureSize * settings.rend.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() && !settings.rend.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;
|
|
|
|
if (mipmapped)
|
|
{
|
|
pb32.init(w, h, true, texcache_scratch(PixelBuffer<u32>::bytes(w, h, true)));
|
|
for (u32 i = 0; i <= tsp.TexU + 3u; i++)
|
|
{
|
|
pb32.set_mipmap(i);
|
|
u32 vram_addr;
|
|
if (tcw.VQ_Comp)
|
|
{
|
|
vram_addr = sa_tex + VQMipPoint[i];
|
|
if (i == 0)
|
|
{
|
|
u32 corner[4];
|
|
PixelBuffer<u32> pb0;
|
|
pb0.init(2, 2, false, corner);
|
|
/* The 1x1 level is the corner of a 2x2 block. For a
|
|
* YUV texture that block is not a colour - it came
|
|
* out magenta, behind the pitch in World Series
|
|
* Baseball 2K2 - and the 2x2 level's is used. */
|
|
if (tcw.PixelFmt == PixelYUV)
|
|
vram_addr = sa_tex + VQMipPoint[1];
|
|
texconv32(&pb0, (u8*)&vram[vram_addr], 2, 2);
|
|
*pb32.data() = *pb0.data(1, 1);
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
vram_addr = sa_tex + OtherMipPoint[i] * tex->bpp / 8;
|
|
if (tcw.PixelFmt == PixelYUV && i == 0)
|
|
// Special case for YUV at 1x1 LoD
|
|
format[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(w, h, false, texcache_scratch(PixelBuffer<u32>::bytes(w, h, false) + stride_over * sizeof(u32)));
|
|
texconv32(&pb32, (u8*)&vram[sa], stride, rows);
|
|
if (rows < h)
|
|
memset((u32 *)pb32.data() + rows * w, 0, (size_t)(h - rows) * w * sizeof(u32));
|
|
if (stride < w)
|
|
for (u32 y = 0; y < h; y++)
|
|
memset(pb32.data(stride, y), 0, (w - stride) * sizeof(u32));
|
|
|
|
#ifdef HAVE_TEXUPSCALE
|
|
// xBRZ scaling
|
|
if (textureUpscaling)
|
|
{
|
|
PixelBuffer<u32> tmp_buf;
|
|
tmp_buf.init(w * settings.rend.TextureUpscale, h * settings.rend.TextureUpscale);
|
|
|
|
if (tcw.PixelFmt == Pixel1555 || tcw.PixelFmt == Pixel4444)
|
|
// Alpha channel formats. Palettes with alpha are already handled
|
|
has_alpha = true;
|
|
UpscalexBRZ(settings.rend.TextureUpscale, pb32.data(), tmp_buf.data(), w, h, has_alpha);
|
|
pb32.steal_data(tmp_buf);
|
|
upscaled_w *= settings.rend.TextureUpscale;
|
|
upscaled_h *= settings.rend.TextureUpscale;
|
|
}
|
|
#endif
|
|
}
|
|
temp_tex_buffer = pb32.data();
|
|
}
|
|
else if (texconv8 != NULL && tex_type == TextureType::_8)
|
|
{
|
|
if (mipmapped)
|
|
{
|
|
pb8.init(w, h, true, texcache_scratch(PixelBuffer<u8>::bytes(w, h, true)));
|
|
for (u32 i = 0; i <= tsp.TexU + 3u; i++)
|
|
{
|
|
pb8.set_mipmap(i);
|
|
u32 vram_addr = sa_tex + OtherMipPoint[i] * tex->bpp / 8;
|
|
texconv8(&pb8, &vram[vram_addr], 1 << i, 1 << i);
|
|
}
|
|
pb8.set_mipmap(0);
|
|
}
|
|
else
|
|
{
|
|
pb8.init(w, h, false, texcache_scratch(PixelBuffer<u8>::bytes(w, h, false)));
|
|
texconv8(&pb8, &vram[sa], stride, rows);
|
|
if (rows < h)
|
|
memset((u8 *)pb8.data() + rows * w, 0, (size_t)(h - rows) * w);
|
|
}
|
|
temp_tex_buffer = pb8.data();
|
|
}
|
|
else if (texconv != NULL)
|
|
{
|
|
if (mipmapped)
|
|
{
|
|
pb16.init(w, h, true, texcache_scratch(PixelBuffer<u16>::bytes(w, h, true)));
|
|
for (u32 i = 0; i <= tsp.TexU + 3u; i++)
|
|
{
|
|
pb16.set_mipmap(i);
|
|
u32 vram_addr;
|
|
if (tcw.VQ_Comp)
|
|
{
|
|
vram_addr = sa_tex + VQMipPoint[i];
|
|
if (i == 0)
|
|
{
|
|
u16 corner[4];
|
|
PixelBuffer<u16> pb0;
|
|
pb0.init(2, 2, false, corner);
|
|
texconv(&pb0, (u8*)&vram[vram_addr], 2, 2);
|
|
*pb16.data() = *pb0.data(1, 1);
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
vram_addr = sa_tex + OtherMipPoint[i] * tex->bpp / 8;
|
|
texconv(&pb16, (u8*)&vram[vram_addr], 1 << i, 1 << i);
|
|
}
|
|
pb16.set_mipmap(0);
|
|
}
|
|
else
|
|
{
|
|
pb16.init(w, h, false, texcache_scratch(PixelBuffer<u16>::bytes(w, h, false) + stride_over * sizeof(u16)));
|
|
texconv(&pb16,(u8*)&vram[sa],stride,rows);
|
|
if (rows < h)
|
|
memset((u16 *)pb16.data() + rows * w, 0, (size_t)(h - rows) * w * sizeof(u16));
|
|
if (stride < w)
|
|
for (u32 y = 0; y < h; y++)
|
|
memset(pb16.data(stride, y), 0, (w - stride) * sizeof(u16));
|
|
}
|
|
temp_tex_buffer = pb16.data();
|
|
}
|
|
else
|
|
{
|
|
//fill it in with a temp color
|
|
WARN_LOG(RENDERER, "UNHANDLED TEXTURE");
|
|
pb16.init(w, h, false, texcache_scratch(PixelBuffer<u16>::bytes(w, h, false)));
|
|
memset(pb16.data(), 0x80, w * h * 2);
|
|
temp_tex_buffer = pb16.data();
|
|
mipmapped = false;
|
|
}
|
|
//lock the texture to detect changes in it, unless it is frequently
|
|
//updated: each update of a protected texture costs a SIGSEGV (the write
|
|
//fault), the invalidation, a full re-upload, and a re-protect -- measured
|
|
//as 1-2 faults per frame sustained, with matching frame-time jitter, on
|
|
//titles that rewrite textures every frame (e.g. VF3tb). Past the update
|
|
//threshold, leave the pages writable and detect changes with a content
|
|
//hash at lookup time instead (see NeedsUpdate): a hash over the same range
|
|
//is cheaper than the fault path, and when the game rewrites identical
|
|
//content it avoids the re-upload entirely.
|
|
if (Updates < FREQUENT_UPDATE_COUNT)
|
|
libCore_vramlock_Lock(sa_tex, sa + size - 1, this);
|
|
else
|
|
vram_hash = ComputeVramHash();
|
|
|
|
UploadToGPU(upscaled_w, upscaled_h, (u8*)temp_tex_buffer, IsMipmapped(), mipmapped);
|
|
if (settings.rend.DumpTextures)
|
|
{
|
|
ComputeHash();
|
|
custom_texture.DumpTexture(texture_hash, upscaled_w, upscaled_h, tex_type, temp_tex_buffer);
|
|
NOTICE_LOG(RENDERER, "Dumped texture %x.png. Old hash %x", texture_hash, old_texture_hash);
|
|
}
|
|
PrintTextureName();
|
|
}
|
|
|
|
void BaseTextureCacheData::CheckCustomTexture()
|
|
{
|
|
if (IsCustomTextureAvailable())
|
|
{
|
|
tex_type = TextureType::_8888;
|
|
UploadToGPU(custom_width, custom_height, custom_image_data, IsMipmapped(), false);
|
|
free(custom_image_data);
|
|
custom_image_data = NULL;
|
|
}
|
|
}
|
|
|
|
void ReadFramebuffer(PixelBuffer<u32>& pb, int& width, int& height)
|
|
{
|
|
width = (FB_R_SIZE.fb_x_size + 1) << 1; // in 16-bit words
|
|
height = FB_R_SIZE.fb_y_size + 1;
|
|
int modulus = (FB_R_SIZE.fb_modulus - 1) << 1;
|
|
|
|
int bpp;
|
|
switch (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 = FB_R_SOF1;
|
|
if (SPG_CONTROL.interlace)
|
|
{
|
|
if (width == modulus && FB_R_SOF2 == FB_R_SOF1 + width * bpp)
|
|
{
|
|
// Typical case alternating even and odd lines -> take the whole buffer at once
|
|
modulus = 0;
|
|
height *= 2;
|
|
}
|
|
else
|
|
{
|
|
addr = SPG_STATUS.fieldnum ? FB_R_SOF2 : FB_R_SOF1;
|
|
}
|
|
}
|
|
else if (FB_R_CTRL.vclk_div == 0)
|
|
{
|
|
/* Not interlaced, on a television: 240 lines are all there are,
|
|
* whatever the size register says. Upstream: the Echelon intro of
|
|
* Confidential Mission (PAL). */
|
|
height = std::min(height, 240);
|
|
}
|
|
|
|
// handed to the GPU at once by whoever asked: converted where textures are, nothing allocated
|
|
pb.init(width, height, false, texcache_scratch(PixelBuffer<u32>::bytes(width, height, false)));
|
|
u8 *dst = (u8*)pb.data();
|
|
|
|
switch (FB_R_CTRL.fb_depth)
|
|
{
|
|
case FBDE_0555: // 555 RGB
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
for (int i = 0; i < width; i++)
|
|
{
|
|
u16 src = pvr_read_area1<u16>(addr);
|
|
*dst++ = (((src >> 10) & 0x1F) << 3) + FB_R_CTRL.fb_concat;
|
|
*dst++ = (((src >> 5) & 0x1F) << 3) + FB_R_CTRL.fb_concat;
|
|
*dst++ = (((src >> 0) & 0x1F) << 3) + FB_R_CTRL.fb_concat;
|
|
*dst++ = 0xFF;
|
|
addr += bpp;
|
|
}
|
|
addr += modulus * bpp;
|
|
}
|
|
break;
|
|
|
|
case FBDE_565: // 565 RGB
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
for (int i = 0; i < width; i++)
|
|
{
|
|
u16 src = pvr_read_area1<u16>(addr);
|
|
*dst++ = (((src >> 11) & 0x1F) << 3) + FB_R_CTRL.fb_concat;
|
|
*dst++ = (((src >> 5) & 0x3F) << 2) + (FB_R_CTRL.fb_concat & 3);
|
|
*dst++ = (((src >> 0) & 0x1F) << 3) + FB_R_CTRL.fb_concat;
|
|
*dst++ = 0xFF;
|
|
addr += bpp;
|
|
}
|
|
addr += modulus * bpp;
|
|
}
|
|
break;
|
|
case FBDE_888: // 888 RGB
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
for (int i = 0; i < width; i += 4)
|
|
{
|
|
u32 src = pvr_read_area1<u32>(addr);
|
|
*dst++ = src >> 16;
|
|
*dst++ = src >> 8;
|
|
*dst++ = src;
|
|
*dst++ = 0xFF;
|
|
addr += 4;
|
|
if (i + 1 >= width)
|
|
break;
|
|
u32 src2 = pvr_read_area1<u32>(addr);
|
|
*dst++ = src2 >> 8;
|
|
*dst++ = src2;
|
|
*dst++ = src >> 24;
|
|
*dst++ = 0xFF;
|
|
addr += 4;
|
|
if (i + 2 >= width)
|
|
break;
|
|
u32 src3 = pvr_read_area1<u32>(addr);
|
|
*dst++ = src3;
|
|
*dst++ = src2 >> 24;
|
|
*dst++ = src2 >> 16;
|
|
*dst++ = 0xFF;
|
|
addr += 4;
|
|
if (i + 3 >= width)
|
|
break;
|
|
*dst++ = src3 >> 24;
|
|
*dst++ = src3 >> 16;
|
|
*dst++ = src3 >> 8;
|
|
*dst++ = 0xFF;
|
|
}
|
|
addr += modulus * bpp;
|
|
}
|
|
break;
|
|
case FBDE_C888: // 0888 RGB
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
for (int i = 0; i < width; i++)
|
|
{
|
|
u32 src = pvr_read_area1<u32>(addr);
|
|
*dst++ = src >> 16;
|
|
*dst++ = src >> 8;
|
|
*dst++ = src;
|
|
*dst++ = 0xFF;
|
|
addr += bpp;
|
|
}
|
|
addr += modulus * bpp;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void rend_text_invl(vram_block* bl)
|
|
{
|
|
BaseTextureCacheData* tcd = (BaseTextureCacheData*)bl->userdata;
|
|
tcd->dirty = FrameCount;
|
|
tcd->lock_block = nullptr;
|
|
|
|
libCore_vramlock_Unlock_block_wb(bl);
|
|
}
|