Files
parallel-dc/core/rend/CustomTexture.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

346 lines
10 KiB
C++

/*
Copyright 2018 flyinghead
This file is part of reicast.
reicast 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.
reicast 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 reicast. If not, see <https://www.gnu.org/licenses/>.
*/
#include "CustomTexture.h"
#include "deps/coreio/coreio.h"
#include <algorithm>
#include <sstream>
#include <sys/stat.h>
#include "retro_dirent.h"
#include "string/stdstring.h"
#define STB_IMAGE_IMPLEMENTATION
#define STBI_ONLY_JPEG
#define STBI_ONLY_PNG
#include <stb_image.h>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
#include "reios/reios.h"
#include "file/file_path.h"
#include "hw/naomi/naomi_cart.h"
extern const char *retro_get_system_directory();
CustomTexture custom_texture;
/* Loader thread: load the custom image for one queued texture. Requests
* that arrive for it meanwhile are not queued again (the count is not
* zero), so go round until the count this pass answered is all there is.
*
* Nothing of the texture is read here but what the request left for it,
* and nothing written but the image and its size, which the renderer
* only looks at once the count is back to zero. The texture's hash used
* to be worked out here, from video memory the game may be writing by
* then and from a texture the renderer may be updating. */
void CustomTexture::Load(BaseTextureCacheData *texture)
{
for (;;)
{
/* The count, and only then the names: see TexCache.h. */
const int requests = retro_atomic_load_acquire_int(&texture->custom_load_in_progress);
const u32 hash = (u32)retro_atomic_load_acquire_int(&texture->custom_request_hash);
const u32 old_vqhash = (u32)retro_atomic_load_acquire_int(&texture->custom_request_old_vqhash);
const u32 old_hash = (u32)retro_atomic_load_acquire_int(&texture->custom_request_old_hash);
int width, height;
u8 *image_data;
if (texture->custom_image_data != NULL)
{
free(texture->custom_image_data);
texture->custom_image_data = NULL;
}
image_data = LoadCustomTexture(hash, width, height);
// under the names it has had before
if (image_data == NULL && old_vqhash != 0)
image_data = LoadCustomTexture(old_vqhash, width, height);
if (image_data == NULL)
image_data = LoadCustomTexture(old_hash, width, height);
if (image_data != NULL)
{
texture->custom_width = width;
texture->custom_height = height;
texture->custom_image_data = image_data;
}
if (retro_atomic_fetch_sub_int(&texture->custom_load_in_progress, requests) == requests)
break;
}
}
void CustomTexture::LoaderThread()
{
LoadMap();
while (retro_atomic_load_acquire_int(&initialized))
{
BaseTextureCacheData *texture = work_queue.TakeAll();
while (texture != NULL)
{
/* Read the link first: once the count is back to zero the
* texture may be queued again, or deleted. */
BaseTextureCacheData *next = texture->custom_load_next;
if (retro_atomic_load_acquire_int(&initialized))
Load(texture);
else
retro_atomic_store_release_int(&texture->custom_load_in_progress, 0);
texture = next;
}
wakeup_thread.Wait();
}
}
std::string CustomTexture::GetGameId()
{
std::string game_id;
if (settings.System == DC_PLATFORM_DREAMCAST)
game_id = std::string(ip_meta.product_number, sizeof(ip_meta.product_number));
else
game_id = naomi_game_id;
const size_t str_end = game_id.find_last_not_of(' ');
if (str_end == std::string::npos)
return "";
game_id = game_id.substr(0, str_end + 1);
std::replace(game_id.begin(), game_id.end(), ' ', '_');
return game_id;
}
bool CustomTexture::Init()
{
if (!retro_atomic_load_relaxed_int(&initialized))
{
retro_atomic_store_release_int(&initialized, 1);
std::string game_id = GetGameId();
if (game_id.length() > 0)
{
textures_path = std::string(retro_get_system_directory()) + "/dc/textures/"
+ game_id + "/";
if (path_is_directory(textures_path.c_str()))
{
INFO_LOG(RENDERER, "Found custom textures directory: %s", textures_path.c_str());
retro_atomic_store_release_int(&custom_textures_available, 1);
loader_thread.Start();
/* No loader, nothing to give work to: a texture queued for
* it would wait for good, and could not be freed. */
if (loader_thread.hThread == NULL)
{
WARN_LOG(RENDERER, "The custom texture loader could not be started");
retro_atomic_store_release_int(&custom_textures_available, 0);
}
}
}
}
return retro_atomic_load_acquire_int(&custom_textures_available) != 0;
}
void CustomTexture::Terminate()
{
if (retro_atomic_load_relaxed_int(&initialized))
{
retro_atomic_store_release_int(&initialized, 0);
wakeup_thread.Set();
loader_thread.WaitToEnd();
/* The loader is gone: drop what it did not get to. */
BaseTextureCacheData *texture = work_queue.TakeAll();
while (texture != NULL)
{
BaseTextureCacheData *next = texture->custom_load_next;
retro_atomic_store_release_int(&texture->custom_load_in_progress, 0);
texture = next;
}
texture_map.clear();
/* The next game starts from nothing: left set, a game with no
* textures of its own inherited "available" from the one before
* and queued work for a loader that was never started. */
retro_atomic_store_release_int(&custom_textures_available, 0);
textures_path.clear();
}
}
u8* CustomTexture::LoadCustomTexture(u32 hash, int& width, int& height)
{
auto it = texture_map.find(hash);
if (it == texture_map.end())
return nullptr;
int n;
core_file* f = core_fopen(it->second.c_str());
const u8* data;
u8* buf = NULL;
u8* image = NULL;
size_t len;
if (!f)
return nullptr;
/* Decoded out of the mapping when there is one, else from one read. */
len = core_fsize(f);
data = core_fmap(f, NULL);
if (!data)
{
buf = (u8*)malloc(len ? len : 1);
if (buf && core_fread_at(f, 0, buf, len) == len)
data = buf;
}
if (data && len <= 0x7fffffff)
{
stbi_set_flip_vertically_on_load(1);
image = stbi_load_from_memory(data, (int)len, &width, &height, &n, STBI_rgb_alpha);
}
free(buf);
core_fclose(f);
return image;
}
/* Renderer: ask for this texture's custom image. Its names are worked out
* here and now - the renderer is updating the texture, and the game is
* held while it does - and left for the loader, before the request is
* counted. */
void CustomTexture::LoadCustomTextureAsync(BaseTextureCacheData *texture_data)
{
if (!Init())
return;
texture_data->ComputeHash();
retro_atomic_store_release_int(&texture_data->custom_request_hash, (int)texture_data->texture_hash);
retro_atomic_store_release_int(&texture_data->custom_request_old_vqhash, (int)texture_data->old_vqtexture_hash);
retro_atomic_store_release_int(&texture_data->custom_request_old_hash, (int)texture_data->old_texture_hash);
if (retro_atomic_fetch_add_int(&texture_data->custom_load_in_progress, 1) == 0)
{
work_queue.Push(texture_data);
wakeup_thread.Set();
}
}
void CustomTexture::DumpTexture(u32 hash, int w, int h, TextureType textype, void *src_buffer)
{
std::string base_dump_dir = get_writable_data_path("/texdump/");
if (!path_is_valid(base_dump_dir.c_str()))
path_mkdir(base_dump_dir.c_str());
std::string game_id = GetGameId();
if (game_id.length() == 0)
return;
base_dump_dir += game_id + "/";
if (!path_is_valid(base_dump_dir.c_str()))
path_mkdir(base_dump_dir.c_str());
std::stringstream path;
path << base_dump_dir << std::hex << hash << ".png";
u16 *src = (u16 *)src_buffer;
u8 *dst_buffer = (u8 *)malloc(w * h * 4); // 32-bit per pixel
u8 *dst = dst_buffer;
for (int y = 0; y < h; y++)
{
switch (textype)
{
case TextureType::_4444:
for (int x = 0; x < w; x++)
{
*dst++ = ((*src >> 12) & 0xF) << 4;
*dst++ = ((*src >> 8) & 0xF) << 4;
*dst++ = ((*src >> 4) & 0xF) << 4;
*dst++ = (*src & 0xF) << 4;
src++;
}
break;
case TextureType::_565:
for (int x = 0; x < w; x++)
{
*dst++ = ((*src >> 11) & 0x1F) << 3;
*dst++ = ((*src >> 5) & 0x3F) << 2;
*dst++ = (*src & 0x1F) << 3;
*dst++ = 255;
src++;
}
break;
case TextureType::_5551:
for (int x = 0; x < w; x++)
{
*dst++ = ((*src >> 11) & 0x1F) << 3;
*dst++ = ((*src >> 6) & 0x1F) << 3;
*dst++ = ((*src >> 1) & 0x1F) << 3;
*dst++ = (*src & 1) ? 255 : 0;
src++;
}
break;
case TextureType::_8888:
for (int x = 0; x < w; x++)
{
*(u32 *)dst = *(u32 *)src;
dst += 4;
src += 2;
}
break;
default:
WARN_LOG(RENDERER, "dumpTexture: unsupported picture format %x", (u32)textype);
free(dst_buffer);
return;
}
}
stbi_flip_vertically_on_write(1);
stbi_write_png(path.str().c_str(), w, h, STBI_rgb_alpha, dst_buffer, 0);
free(dst_buffer);
}
void CustomTexture::LoadMap()
{
texture_map.clear();
RDIR *dir = retro_opendir(textures_path.c_str());
if (dir == nullptr)
return;
while (true)
{
if (!retro_readdir(dir))
break;
if (retro_dirent_is_dir(dir, nullptr))
continue;
std::string name = retro_dirent_get_name(dir);
std::string child_path = textures_path + name;
const char *extension_ro = path_get_extension(name.c_str());
char extension[5];
strncpy(extension, extension_ro, 4);
extension[4] = '\0';
string_to_lower(extension);
if (strcmp(extension, "jpg") && strcmp(extension, "jpeg") && strcmp(extension, "png"))
continue;
std::string::size_type dotpos = name.find_last_of('.');
std::string basename = name.substr(0, dotpos);
char *endptr;
u32 hash = (u32)strtoll(basename.c_str(), &endptr, 16);
if (endptr - basename.c_str() < (ptrdiff_t)basename.length())
{
INFO_LOG(RENDERER, "Invalid hash %s", basename.c_str());
continue;
}
texture_map[hash] = child_path;
}
retro_closedir(dir);
retro_atomic_store_release_int(&custom_textures_available, !texture_map.empty());
}