Files
parallel-dc/core/rend/TexCache.cpp
T
LibretroAdmin 4a3f2e1316 custom textures: the renderer works out a texture's names, the loader only reads the request
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.
2026-10-08 08:46:00 +02:00

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