940 lines
26 KiB
C
940 lines
26 KiB
C
/*
|
|
* This software is licensed under the terms of the MIT License.
|
|
* See COPYING for further information.
|
|
* ---
|
|
* Copyright (c) 2011-2024, Lukas Weber <laochailan@web.de>.
|
|
* Copyright (c) 2012-2024, Andrei Alexeyev <akari@taisei-project.org>.
|
|
*/
|
|
|
|
#include "basisu.h"
|
|
#include "basisu_cache.h"
|
|
|
|
#include "rwops/rwops_sha256.h"
|
|
#include "util.h"
|
|
#include "util/env.h"
|
|
#include "util/io.h"
|
|
|
|
#include <basisu_transcoder_c_api.h>
|
|
|
|
// NOTE: sha256sum + hyphen + base16 64-bit file size
|
|
#define BASISU_HASH_SIZE (SHA256_HEXDIGEST_SIZE + 17)
|
|
|
|
enum {
|
|
BASISU_TAISEI_ID = 0x52656900,
|
|
BASISU_TAISEI_CHANNELS_R = 0,
|
|
BASISU_TAISEI_CHANNELS_RG = 1,
|
|
BASISU_TAISEI_CHANNELS_RGB = 2,
|
|
BASISU_TAISEI_CHANNELS_RGBA = 3,
|
|
BASISU_TAISEI_CHANNELS_MASK = 0x3,
|
|
BASISU_TAISEI_SRGB = (BASISU_TAISEI_CHANNELS_MASK + 1) << 0,
|
|
BASISU_TAISEI_NORMALMAP = (BASISU_TAISEI_CHANNELS_MASK + 1) << 1,
|
|
BASISU_TAISEI_GRAYALPHA = (BASISU_TAISEI_CHANNELS_MASK + 1) << 2,
|
|
};
|
|
|
|
struct basis_size_info {
|
|
uint32_t num_blocks;
|
|
uint32_t block_size;
|
|
};
|
|
|
|
#define BASIS_EXT ".basis"
|
|
|
|
char *texture_loader_basisu_try_path(const char *basename) {
|
|
return try_path(TEX_PATH_PREFIX, basename, BASIS_EXT);
|
|
}
|
|
|
|
bool texture_loader_basisu_check_path(const char *path) {
|
|
return strendswith(path, BASIS_EXT);
|
|
}
|
|
|
|
static void texture_loader_basisu_tls_destructor(void *tc) {
|
|
basist_transcoder_destroy(NOT_NULL(tc));
|
|
}
|
|
|
|
static basist_transcoder *texture_loader_basisu_get_transcoder(void) {
|
|
static SDL_SpinLock lock;
|
|
static SDL_TLSID tls;
|
|
static basist_transcoder *fallback;
|
|
|
|
if(UNLIKELY(fallback)) {
|
|
return fallback;
|
|
}
|
|
|
|
if(UNLIKELY(!tls)) {
|
|
SDL_AtomicLock(&lock);
|
|
if(!tls) {
|
|
tls = SDL_TLSCreate();
|
|
}
|
|
SDL_AtomicUnlock(&lock);
|
|
}
|
|
|
|
basist_transcoder *tc;
|
|
|
|
if(LIKELY(tls)) {
|
|
tc = SDL_TLSGet(tls);
|
|
} else {
|
|
tc = fallback;
|
|
}
|
|
|
|
if(LIKELY(tc)) {
|
|
return tc;
|
|
}
|
|
|
|
tc = basist_transcoder_create();
|
|
|
|
if(UNLIKELY(tc == NULL)) {
|
|
log_error("basist_transcoder_create() failed");
|
|
return NULL;
|
|
}
|
|
|
|
BASISU_DEBUG("Created Basis Universal transcoder %p for thread %p", (void*)tc, (void*)SDL_ThreadID());
|
|
|
|
if(LIKELY(tls)) {
|
|
SDL_TLSSet(tls, tc, texture_loader_basisu_tls_destructor);
|
|
} else {
|
|
// This "leaks"; I don't care. The allocation should only happen once.
|
|
fallback = tc;
|
|
}
|
|
|
|
return tc;
|
|
}
|
|
|
|
static const char *get_basis_source_format_name(basist_source_format sfmt) {
|
|
switch(sfmt) {
|
|
case BASIST_SOURCE_ETC1S: return "ETC1S";
|
|
case BASIST_SOURCE_UASTC4x4: return "UASTC";
|
|
default: return "(unknown)";
|
|
}
|
|
}
|
|
|
|
static basist_texture_format compfmt_pixmap_to_basist(PixmapFormat fmt) {
|
|
switch(fmt) {
|
|
#define HANDLE(comp, layout, ...) \
|
|
case PIXMAP_FORMAT_##comp: return BASIST_FORMAT_##comp;
|
|
PIXMAP_COMPRESSION_FORMATS(HANDLE,)
|
|
#undef HANDLE
|
|
default: UNREACHABLE;
|
|
}
|
|
}
|
|
|
|
static struct basis_size_info texture_loader_basisu_get_transcoded_size_info(
|
|
TextureLoadData *ld,
|
|
basist_transcoder *tc,
|
|
uint32_t image,
|
|
uint32_t level,
|
|
basist_texture_format format
|
|
) {
|
|
struct basis_size_info size_info = { 0 };
|
|
|
|
if((uint)format >= BASIST_NUM_FORMATS) {
|
|
log_error("%s: Invalid basisu format 0x%04x", ld->st->name, format);
|
|
}
|
|
|
|
basist_source_format src_format = basist_transcoder_get_source_format(tc);
|
|
|
|
if(!basist_is_format_supported(format, src_format)) {
|
|
log_error("%s: Can't transcode from %s into %s",
|
|
ld->src_paths.main,
|
|
get_basis_source_format_name(src_format),
|
|
basist_get_format_name(format)
|
|
);
|
|
return size_info;
|
|
}
|
|
|
|
basist_image_level_desc ldesc;
|
|
|
|
if(!basist_transcoder_get_image_level_desc(tc, image, level, &ldesc)) {
|
|
log_error("%s: basist_transcoder_get_image_level_desc() failed", ld->st->name);
|
|
return size_info;
|
|
}
|
|
|
|
if(basist_is_format_uncompressed(format)) {
|
|
size_info.block_size = basist_get_uncompressed_bytes_per_pixel(format);
|
|
size_info.num_blocks = ldesc.orig_width * ldesc.orig_height;
|
|
} else {
|
|
// NOTE: there's a caveat for PVRTC1 textures, but we don't care about them
|
|
size_info.block_size = basist_get_bytes_per_block_or_pixel(format);
|
|
size_info.num_blocks = ldesc.total_blocks;
|
|
}
|
|
|
|
if(size_info.num_blocks > PIXMAP_BUFFER_MAX_SIZE / size_info.block_size) {
|
|
log_error("%s: Image %u level %u is too large",
|
|
ld->st->name,
|
|
image,
|
|
level
|
|
);
|
|
memset(&size_info, 0, sizeof(size_info));
|
|
}
|
|
|
|
return size_info;
|
|
}
|
|
|
|
static PixmapFormat texture_loader_basisu_pick_and_apply_compressed_format(
|
|
TextureLoadData *ld,
|
|
uint32_t taisei_meta,
|
|
basist_source_format basis_source_fmt,
|
|
PixmapOrigin org,
|
|
PixmapFormat fallback_format,
|
|
TextureTypeQueryResult *out_qr
|
|
) {
|
|
const char *ctx = ld->st->name;
|
|
TextureTypeQueryResult qr;
|
|
|
|
if(env_get("TAISEI_BASISU_FORCE_UNCOMPRESSED", false)) {
|
|
log_info("%s: Uncompressed fallback forced by environment", ctx);
|
|
goto uncompressed;
|
|
}
|
|
|
|
static int fmt_prio[4][BASIST_NUM_FORMATS] = {
|
|
/*
|
|
* NOTE: Some key considerations:
|
|
*
|
|
* - This priority assumes source textures are in ETC1S format, not UASTC.
|
|
*
|
|
* - ETC formats are the fastest to transcode to, because ETC1S is essentially a ETC1/ETC2 subset.
|
|
*
|
|
* - ETC formats are usually not supported by desktop GPUs.
|
|
*
|
|
* - Desktop OpenGL usually exposes ETC support as part of the ARB_ES3_compatibility extension.
|
|
*
|
|
* - ETC on desktop is usually emulated as an uncompressed format. Uploading ETC textures in this case is
|
|
* INCREDIBLY SLOW, at least in Mesa, because the GL driver is forced to decompress them on the main thread,
|
|
* sequentially. This completely negates the transcoding speed advantage of ETC.
|
|
*
|
|
* - There is no reliable way in OpenGL to query whether a compressed format is emulated or not.
|
|
*
|
|
* - ASTC is also rarely available on the desktop. It's required by ES 3.2, but not by ARB_ES3_2_compatibility.
|
|
* Despite this, ASTC support is still often emulated on desktop GL. However, ASTC uploads appear to be much
|
|
* faster than ETC ones (on Mesa).
|
|
*
|
|
* - S3TC (which covers BC1, BC3, BC4, BC5) is near-universal on desktop and almost-nonexistent on mobile. BC7
|
|
* (also known as BPTC) is not as widespread, but the situation is similar.
|
|
*
|
|
* - BC7/BPTC preserves quality better than BC1 and BC3, at the cost of being a bit larger.
|
|
*
|
|
* - Despite emulated ASTC being much faster to upload on desktop, I've placed it below ETC in the priority
|
|
* lists. This is because on the desktop, one of the BC* formats will be picked 99% of the time anyway, and
|
|
* if not, then ASTC is unlikely to be available either. On mobile, ETC has an advantage because of its fast * * transcode speed, lower bitrate, and lossless transcoding.
|
|
*
|
|
* - ATC and FXT1 are old and obscure. ATC appears to have some presence on mobile. FXT1 is effectively Intel-
|
|
* only. Both are not expected to be emulated.
|
|
*
|
|
* - PVRTC is mostly limited to Apple's mobile devices, and also is not expected to be emulated.
|
|
*
|
|
* - PVRTC1 requires textures to be power-of-two quads, is slow to transcode to, and is generally inferior to
|
|
* PVRTC2.
|
|
*
|
|
* - I have been unable to test PVRTC1, PVRTC2, ATC, and FXT1 so far.
|
|
*/
|
|
|
|
[BASISU_TAISEI_CHANNELS_R] = {
|
|
PIXMAP_FORMAT_BC4_R,
|
|
PIXMAP_FORMAT_BC5_RG,
|
|
PIXMAP_FORMAT_BC7_RGBA,
|
|
PIXMAP_FORMAT_BC1_RGB,
|
|
PIXMAP_FORMAT_BC3_RGBA,
|
|
PIXMAP_FORMAT_PVRTC2_4_RGB,
|
|
PIXMAP_FORMAT_PVRTC2_4_RGBA,
|
|
// PIXMAP_FORMAT_PVRTC1_4_RGB,
|
|
// PIXMAP_FORMAT_PVRTC1_4_RGBA,
|
|
PIXMAP_FORMAT_ATC_RGB,
|
|
PIXMAP_FORMAT_ATC_RGBA,
|
|
PIXMAP_FORMAT_FXT1_RGB,
|
|
PIXMAP_FORMAT_ETC2_EAC_R11,
|
|
PIXMAP_FORMAT_ETC2_EAC_RG11,
|
|
PIXMAP_FORMAT_ETC1_RGB,
|
|
PIXMAP_FORMAT_ETC2_RGBA,
|
|
PIXMAP_FORMAT_ASTC_4x4_RGBA,
|
|
0,
|
|
},
|
|
[BASISU_TAISEI_CHANNELS_RG] = {
|
|
// NOTE: most (all?) RGB formats are unsuitable for normalmaps/non-color data due to "crosstalk" between the
|
|
// channels. RG formats are specifically meant for this, but RGBA formats also work. In the RGBA case we
|
|
// use the color part for the R channel and the alpha part for the G channel, and use a swizzle mask to
|
|
// transparently correct sampling.
|
|
PIXMAP_FORMAT_BC5_RG,
|
|
PIXMAP_FORMAT_BC7_RGBA,
|
|
PIXMAP_FORMAT_BC3_RGBA,
|
|
PIXMAP_FORMAT_PVRTC2_4_RGBA,
|
|
// PIXMAP_FORMAT_PVRTC1_4_RGBA,
|
|
PIXMAP_FORMAT_ATC_RGBA,
|
|
PIXMAP_FORMAT_ETC2_EAC_RG11,
|
|
PIXMAP_FORMAT_ETC2_RGBA,
|
|
PIXMAP_FORMAT_ASTC_4x4_RGBA,
|
|
0,
|
|
},
|
|
[BASISU_TAISEI_CHANNELS_RGB] = {
|
|
PIXMAP_FORMAT_BC7_RGBA,
|
|
PIXMAP_FORMAT_BC1_RGB,
|
|
PIXMAP_FORMAT_BC3_RGBA,
|
|
PIXMAP_FORMAT_PVRTC2_4_RGB,
|
|
PIXMAP_FORMAT_PVRTC2_4_RGBA,
|
|
// PIXMAP_FORMAT_PVRTC1_4_RGB,
|
|
// PIXMAP_FORMAT_PVRTC1_4_RGBA,
|
|
PIXMAP_FORMAT_ATC_RGB,
|
|
PIXMAP_FORMAT_ATC_RGBA,
|
|
PIXMAP_FORMAT_FXT1_RGB,
|
|
PIXMAP_FORMAT_ETC1_RGB,
|
|
PIXMAP_FORMAT_ETC2_RGBA,
|
|
PIXMAP_FORMAT_ASTC_4x4_RGBA,
|
|
0,
|
|
},
|
|
[BASISU_TAISEI_CHANNELS_RGBA] = {
|
|
PIXMAP_FORMAT_BC7_RGBA,
|
|
PIXMAP_FORMAT_BC3_RGBA,
|
|
PIXMAP_FORMAT_PVRTC2_4_RGBA,
|
|
// PIXMAP_FORMAT_PVRTC1_4_RGBA,
|
|
PIXMAP_FORMAT_ATC_RGBA,
|
|
PIXMAP_FORMAT_ETC2_RGBA,
|
|
PIXMAP_FORMAT_ASTC_4x4_RGBA,
|
|
0,
|
|
},
|
|
};
|
|
|
|
uint chan_idx = taisei_meta & BASISU_TAISEI_CHANNELS_MASK;
|
|
assert(chan_idx < ARRAY_SIZE(fmt_prio));
|
|
|
|
bool swizzle_supported = r_supports(RFEAT_TEXTURE_SWIZZLE);
|
|
|
|
for(int *pfmt = fmt_prio[chan_idx]; *pfmt; ++pfmt) {
|
|
PixmapFormat px_fmt = *pfmt;
|
|
basist_texture_format basist_fmt = compfmt_pixmap_to_basist(px_fmt);
|
|
|
|
BASISU_DEBUG("%s: Try %s", ctx, pixmap_format_name(px_fmt));
|
|
|
|
if(!basist_is_format_supported(basist_fmt, basis_source_fmt)) {
|
|
BASISU_DEBUG("%s: Skip: can't transcode from %s into %s",
|
|
ctx,
|
|
get_basis_source_format_name(basis_source_fmt),
|
|
basist_get_format_name(basist_fmt)
|
|
);
|
|
|
|
continue;
|
|
}
|
|
|
|
if(chan_idx == BASISU_TAISEI_CHANNELS_RG && !swizzle_supported) {
|
|
uint nchans = pixmap_format_layout(px_fmt);
|
|
|
|
if(taisei_meta & BASISU_TAISEI_GRAYALPHA) {
|
|
if(nchans == 2) {
|
|
BASISU_DEBUG("%s: Skip: can't swizzle RG01 into RRRG", ctx);
|
|
continue;
|
|
}
|
|
} else {
|
|
if(nchans == 4) {
|
|
BASISU_DEBUG("%s: Skip: can't swizzle RGBA into GA01", ctx);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
TextureType tex_type = r_texture_type_from_pixmap_format(px_fmt);
|
|
if(!texture_loader_try_set_texture_type(ld, tex_type, px_fmt, org, false, &qr)) {
|
|
BASISU_DEBUG("%s: Skip: texture type %s not supported by renderer", ctx, r_texture_type_name(tex_type));
|
|
continue;
|
|
}
|
|
|
|
if(!qr.supplied_pixmap_origin_supported) {
|
|
BASISU_DEBUG("%s: Skip: supplied origin not supported; flipping not implemented", ctx);
|
|
continue;
|
|
}
|
|
|
|
assert(qr.supplied_pixmap_format_supported);
|
|
|
|
ld->params.type = tex_type;
|
|
|
|
if(out_qr) {
|
|
*out_qr = qr;
|
|
}
|
|
|
|
log_info("%s: Transcoding into %s", ctx, basist_get_format_name(basist_fmt));
|
|
return px_fmt;
|
|
}
|
|
|
|
log_warn("%s: No suitable compression format available; falling back to uncompressed RGBA", ctx);
|
|
|
|
uncompressed:
|
|
(void)0; // C is dumb
|
|
|
|
TextureType tex_type = r_texture_type_from_pixmap_format(fallback_format);
|
|
if(texture_loader_set_texture_type_uncompressed(ld, tex_type, PIXMAP_FORMAT_RGBA8, org, &qr)) {
|
|
if(out_qr) {
|
|
*out_qr = qr;
|
|
}
|
|
|
|
assert(qr.supplied_pixmap_format_supported);
|
|
return qr.optimal_pixmap_format;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
struct basisu_load_data {
|
|
char *filebuf;
|
|
basist_transcoder *tc;
|
|
uint mip_bias;
|
|
PixmapFormat px_decode_format;
|
|
PixmapOrigin px_origin;
|
|
bool transcoding_started;
|
|
bool swizzle_supported;
|
|
bool is_uncompressed_fallback;
|
|
char basis_hash[BASISU_HASH_SIZE];
|
|
};
|
|
|
|
static void texture_loader_basisu_cleanup(struct basisu_load_data *bld) {
|
|
if(bld->tc) {
|
|
if(basist_transcoder_get_ready_to_transcode(bld->tc)) {
|
|
basist_transcoder_stop_transcoding(bld->tc);
|
|
}
|
|
|
|
basist_transcoder_set_data(bld->tc, (basist_data) { 0 });
|
|
}
|
|
|
|
mem_free(bld->filebuf);
|
|
bld->filebuf = NULL;
|
|
}
|
|
|
|
static void texture_loader_basisu_failed(TextureLoadData *ld, struct basisu_load_data *bld) {
|
|
texture_loader_basisu_cleanup(bld);
|
|
texture_loader_failed(ld);
|
|
}
|
|
|
|
static char *read_basis_file(SDL_RWops *rw, size_t *file_size, size_t hash_size, char hash[hash_size]) {
|
|
assert(hash_size >= BASISU_HASH_SIZE);
|
|
|
|
SHA256State *sha256 = sha256_new();
|
|
rw = SDL_RWWrapSHA256(rw, sha256, false);
|
|
char *buf = NULL;
|
|
|
|
if(LIKELY(rw)) {
|
|
buf = SDL_RWreadAll(rw, file_size, INT32_MAX);
|
|
SDL_RWclose(rw);
|
|
}
|
|
|
|
if(UNLIKELY(!buf)) {
|
|
*file_size = 0;
|
|
sha256_free(sha256);
|
|
return NULL;
|
|
}
|
|
|
|
uint8_t raw_hash[SHA256_BLOCK_SIZE];
|
|
sha256_final(sha256, raw_hash, sizeof(raw_hash));
|
|
sha256_free(sha256);
|
|
hexdigest(raw_hash, sizeof(raw_hash), hash, hash_size);
|
|
|
|
assert(hash[SHA256_HEXDIGEST_SIZE - 1] == 0);
|
|
snprintf(&hash[SHA256_HEXDIGEST_SIZE - 1], BASISU_HASH_SIZE - SHA256_HEXDIGEST_SIZE, "-%zx", *file_size);
|
|
|
|
return buf;
|
|
}
|
|
|
|
static void texture_loader_basisu_set_swizzle(TextureLoadData *ld, PixmapFormat fmt, uint32_t taisei_meta) {
|
|
PixmapLayout channels = pixmap_format_layout(fmt);
|
|
|
|
switch(taisei_meta & BASISU_TAISEI_CHANNELS_MASK) {
|
|
// NOTE: Using the green channel for monochrome data ensures best precision,
|
|
// because color endpoints are stored as RGB565 in some formats.
|
|
|
|
case BASISU_TAISEI_CHANNELS_R:
|
|
if(channels == PIXMAP_LAYOUT_R) {
|
|
ld->params.swizzle = (SwizzleMask) { "rrr1" };
|
|
} else {
|
|
ld->params.swizzle = (SwizzleMask) { "ggg1" };
|
|
}
|
|
break;
|
|
|
|
case BASISU_TAISEI_CHANNELS_RG:
|
|
if(channels == PIXMAP_LAYOUT_RGBA) {
|
|
if(taisei_meta & BASISU_TAISEI_GRAYALPHA) {
|
|
ld->params.swizzle = (SwizzleMask) { "ggga" };
|
|
} else {
|
|
ld->params.swizzle = (SwizzleMask) { "ga01" };
|
|
}
|
|
} else {
|
|
assert(channels == PIXMAP_LAYOUT_RG);
|
|
if(taisei_meta & BASISU_TAISEI_GRAYALPHA) {
|
|
ld->params.swizzle = (SwizzleMask) { "rrrg" };
|
|
}
|
|
}
|
|
break;
|
|
|
|
case BASISU_TAISEI_CHANNELS_RGB:
|
|
if(channels == PIXMAP_LAYOUT_RGBA) {
|
|
ld->params.swizzle = (SwizzleMask) { "rgb1" };
|
|
} else {
|
|
assert(channels == PIXMAP_LAYOUT_RGB);
|
|
}
|
|
break;
|
|
|
|
case BASISU_TAISEI_CHANNELS_RGBA:
|
|
break;
|
|
|
|
default:
|
|
UNREACHABLE;
|
|
}
|
|
}
|
|
|
|
#define TRY(func, ...) \
|
|
if(UNLIKELY(!(func(__VA_ARGS__)))) { \
|
|
log_error("%s: " #func "() failed", ctx); \
|
|
texture_loader_basisu_failed(ld, &bld); \
|
|
return; \
|
|
}
|
|
|
|
#define TRY_SILENT(func, ...) \
|
|
if(UNLIKELY(!(func(__VA_ARGS__)))) { \
|
|
texture_loader_basisu_failed(ld, &bld); \
|
|
return; \
|
|
}
|
|
|
|
#define TRY_BOOL(func, ...) \
|
|
if(UNLIKELY(!(func(__VA_ARGS__)))) { \
|
|
log_error("%s: " #func "() failed", ctx); \
|
|
return false; \
|
|
}
|
|
|
|
static bool texture_loader_basisu_check_consistency(
|
|
const char *ctx,
|
|
basist_transcoder *tc,
|
|
basist_file_info *file_info,
|
|
basist_image_info img_infos[]
|
|
) {
|
|
basist_image_info *ref = img_infos;
|
|
|
|
if(ref->total_levels < 1) {
|
|
log_error("%s: No levels in image 0 in Basis Universal texture", ctx);
|
|
return false;
|
|
}
|
|
|
|
bool ok = true;
|
|
|
|
for(uint i = 1; i < file_info->total_images; ++i) {
|
|
basist_image_info *img = img_infos + i;
|
|
|
|
if(img->width != ref->width || img->height != ref->height) {
|
|
log_error(
|
|
"%s: Image %i size is different from image 0 (%ux%u != %ux%u); "
|
|
"this is not allowed",
|
|
ctx, i, img->width, img->height, ref->width, ref->height
|
|
);
|
|
ok = false;
|
|
}
|
|
|
|
if(img->total_levels != ref->total_levels) {
|
|
log_error(
|
|
"%s: Image %i has different number of mip levels from image 0 (%u != %u); "
|
|
"this is not allowed",
|
|
ctx, i, img->total_levels, ref->total_levels
|
|
);
|
|
ok = false;
|
|
}
|
|
|
|
// TODO: check if level dimensions are sane?
|
|
}
|
|
|
|
return ok;
|
|
}
|
|
|
|
static bool texture_loader_basisu_sanitize_levels(
|
|
const char *ctx,
|
|
basist_transcoder *tc,
|
|
const basist_image_info *image_info,
|
|
uint *out_total_levels
|
|
) {
|
|
/*
|
|
* NOTE: We assume the renderer wants all mip levels of compressed textures to have
|
|
* dimensions divisible by 4 (if dimension > 2). This is true for most (all?) BC[1-7]
|
|
* implementations in GLES, but other formats and/or renderers may have more lax requirements
|
|
* (core GL with ARB_texture_compression_{s3tc,bptc} in particular), or even stricter
|
|
* requirements.
|
|
*
|
|
* TODO: Add a renderer API to query such requirements on a per-format basis, and use it
|
|
* here.
|
|
*/
|
|
|
|
for(uint i = 1; i < image_info->total_levels; ++i) {
|
|
basist_image_level_desc level_desc;
|
|
TRY_BOOL(
|
|
basist_transcoder_get_image_level_desc, tc, image_info->image_index, i, &level_desc
|
|
);
|
|
|
|
if(
|
|
(level_desc.orig_width > 2 && level_desc.orig_width % 4) ||
|
|
(level_desc.orig_height > 2 && level_desc.orig_height % 4)
|
|
) {
|
|
log_warn(
|
|
"%s: Mip level %i dimensions are not multiples of 4 (%ix%i); "
|
|
"number of levels reduced %i -> %i",
|
|
ctx, i, level_desc.orig_width, level_desc.orig_height, image_info->total_levels, i
|
|
);
|
|
*out_total_levels = i;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
*out_total_levels = image_info->total_levels;
|
|
return true;
|
|
}
|
|
|
|
static bool texture_loader_basisu_init_mipmaps(
|
|
const char *ctx,
|
|
struct basisu_load_data *bld,
|
|
TextureLoadData *ld,
|
|
basist_image_info *ref_img
|
|
) {
|
|
uint total_levels;
|
|
|
|
if(bld->is_uncompressed_fallback) {
|
|
total_levels = ref_img->total_levels;
|
|
} else {
|
|
TRY_BOOL(texture_loader_basisu_sanitize_levels, ctx, bld->tc, ref_img, &total_levels);
|
|
}
|
|
|
|
int mip_bias = env_get("TAISEI_BASISU_MIP_BIAS", 0);
|
|
mip_bias = clamp(mip_bias, 0, total_levels - 1);
|
|
{
|
|
int max_levels = env_get("TAISEI_BASISU_MAX_MIP_LEVELS", 0);
|
|
if(max_levels > 0) {
|
|
total_levels = clamp(total_levels, 1, mip_bias + max_levels);
|
|
}
|
|
}
|
|
bld->mip_bias = mip_bias;
|
|
|
|
ld->params.mipmaps = total_levels - mip_bias;
|
|
ld->params.mipmap_mode = TEX_MIPMAP_MANUAL;
|
|
|
|
basist_image_level_desc zero_level;
|
|
TRY_BOOL(
|
|
basist_transcoder_get_image_level_desc,
|
|
bld->tc, ref_img->image_index, mip_bias, &zero_level
|
|
);
|
|
|
|
uint max_mips = r_texture_util_max_num_miplevels(zero_level.orig_width, zero_level.orig_height);
|
|
|
|
if(
|
|
ld->params.mipmaps > 1 &&
|
|
ld->params.mipmaps < max_mips &&
|
|
!r_supports(RFEAT_PARTIAL_MIPMAPS)
|
|
) {
|
|
log_warn(
|
|
"%s: Texture has partial mipmaps (%i levels out of %i), "
|
|
"but the renderer doesn't support this. "
|
|
"Mipmapping will be disabled for this texture",
|
|
ctx, ld->params.mipmaps, max_mips
|
|
);
|
|
|
|
ld->params.mipmaps = 1;
|
|
|
|
// TODO: In case of decompression fallback,
|
|
// we could switch to auto-generated mipmaps here instead.
|
|
// Untested code follows:
|
|
#if 0
|
|
if(bld->is_uncompressed_fallback) {
|
|
ld->params.mipmaps = max_mips;
|
|
ld->params.mipmap_mode = TEX_MIPMAP_AUTO;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
switch(ld->params.class) {
|
|
case TEXTURE_CLASS_2D:
|
|
ld->pixmaps = ALLOC_ARRAY(ld->params.mipmaps, typeof(*ld->pixmaps));
|
|
ld->num_pixmaps = ld->params.mipmaps;
|
|
break;
|
|
|
|
case TEXTURE_CLASS_CUBEMAP:
|
|
ld->cubemaps = ALLOC_ARRAY(ld->params.mipmaps, typeof(*ld->cubemaps));
|
|
ld->num_pixmaps = ld->params.mipmaps * 6;
|
|
break;
|
|
|
|
default: UNREACHABLE;
|
|
}
|
|
|
|
ld->params.width = zero_level.orig_width;
|
|
ld->params.height = zero_level.orig_height;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool texture_loader_basisu_load_pixmap(
|
|
const char *ctx,
|
|
struct basisu_load_data *bld,
|
|
TextureLoadData *ld,
|
|
basist_transcode_level_params *parm,
|
|
Pixmap *out_pixmap
|
|
) {
|
|
basist_image_level_desc level_desc;
|
|
TRY_BOOL(
|
|
basist_transcoder_get_image_level_desc,
|
|
bld->tc, parm->image_index, parm->level_index, &level_desc
|
|
);
|
|
|
|
struct basis_size_info size_info = texture_loader_basisu_get_transcoded_size_info(
|
|
ld, bld->tc, parm->image_index, parm->level_index, parm->format
|
|
);
|
|
|
|
if(size_info.block_size == 0) {
|
|
return false;
|
|
}
|
|
|
|
BASISU_DEBUG("Image %i Level %i [%ix%i] : %i * %i = %i",
|
|
parm->image_index, parm->level_index,
|
|
level_desc.orig_width, level_desc.orig_height,
|
|
size_info.num_blocks, size_info.block_size,
|
|
size_info.num_blocks * size_info.block_size
|
|
);
|
|
|
|
uint32_t data_size = size_info.num_blocks * size_info.block_size;
|
|
|
|
if(!texture_loader_basisu_load_cached(
|
|
bld->basis_hash,
|
|
parm,
|
|
&level_desc,
|
|
bld->px_decode_format,
|
|
bld->px_origin,
|
|
data_size,
|
|
out_pixmap
|
|
)) {
|
|
if(!bld->transcoding_started) {
|
|
TRY_BOOL(basist_transcoder_start_transcoding, bld->tc);
|
|
bld->transcoding_started = true;
|
|
}
|
|
|
|
out_pixmap->data_size = data_size;
|
|
out_pixmap->data.untyped = mem_alloc(out_pixmap->data_size);
|
|
parm->output_blocks = out_pixmap->data.untyped;
|
|
parm->output_blocks_size = size_info.num_blocks;
|
|
|
|
TRY_BOOL(basist_transcoder_transcode_image_level, bld->tc, parm);
|
|
|
|
out_pixmap->format = bld->px_decode_format;
|
|
out_pixmap->width = level_desc.orig_width;
|
|
out_pixmap->height = level_desc.orig_height;
|
|
out_pixmap->origin = bld->px_origin;
|
|
|
|
texture_loader_basisu_cache(bld->basis_hash, parm, &level_desc, out_pixmap);
|
|
}
|
|
|
|
if(bld->is_uncompressed_fallback) {
|
|
// TODO: maybe cache the swizzle result?
|
|
if(!bld->swizzle_supported) {
|
|
pixmap_swizzle_inplace(out_pixmap, ld->params.swizzle);
|
|
}
|
|
|
|
// NOTE: it doesn't hurt to call this in the compressed case as well,
|
|
// but it's effectively a no-op with a redundant texture type query.
|
|
|
|
if(!texture_loader_prepare_pixmaps(
|
|
ld, out_pixmap, NULL, ld->params.type, ld->params.flags
|
|
)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void texture_loader_basisu(TextureLoadData *ld) {
|
|
struct basisu_load_data bld = { 0 };
|
|
|
|
if(UNLIKELY(!(bld.tc = texture_loader_basisu_get_transcoder()))) {
|
|
texture_loader_basisu_failed(ld, &bld);
|
|
return;
|
|
}
|
|
|
|
const char *ctx = ld->st->name;
|
|
const char *basis_file = ld->src_paths.main;
|
|
|
|
SDL_RWops *rw_in = res_open_file(ld->st, basis_file, VFS_MODE_READ);
|
|
|
|
if(!UNLIKELY(rw_in)) {
|
|
log_error("%s: VFS error: %s", ctx, vfs_get_error());
|
|
texture_loader_basisu_failed(ld, &bld);
|
|
return;
|
|
}
|
|
|
|
size_t filesize;
|
|
bld.filebuf = read_basis_file(rw_in, &filesize, sizeof(bld.basis_hash), bld.basis_hash);
|
|
SDL_RWclose(rw_in);
|
|
|
|
if(UNLIKELY(!bld.filebuf)) {
|
|
log_error("%s: Read error: %s", basis_file, SDL_GetError());
|
|
texture_loader_basisu_failed(ld, &bld);
|
|
return;
|
|
}
|
|
|
|
assert(!basist_transcoder_get_ready_to_transcode(bld.tc));
|
|
|
|
basist_transcoder_set_data(bld.tc, (basist_data) { .data = bld.filebuf, .size = filesize });
|
|
log_info("%s: Loaded Basis Universal data from %s", ctx, basis_file);
|
|
|
|
basist_file_info file_info = { 0 };
|
|
TRY(basist_transcoder_get_file_info, bld.tc, &file_info);
|
|
|
|
BASISU_DEBUG("Version: %u", file_info.version);
|
|
BASISU_DEBUG("Header size: %u", file_info.total_header_size);
|
|
BASISU_DEBUG("Source format: %s", get_basis_source_format_name(file_info.source_format));
|
|
BASISU_DEBUG("Y-flipped: %u", file_info.y_flipped);
|
|
BASISU_DEBUG("Total images: %u", file_info.total_images);
|
|
BASISU_DEBUG("Total slices: %u", file_info.total_slices);
|
|
BASISU_DEBUG("Has alpha slices: %u", file_info.has_alpha_slices);
|
|
BASISU_DEBUG("Userdata0: 0x%08x", file_info.userdata.userdata[0]);
|
|
BASISU_DEBUG("Userdata1: 0x%08x", file_info.userdata.userdata[1]);
|
|
|
|
if(file_info.total_images < 1) {
|
|
log_error("%s: No images in Basis Universal texture", ctx);
|
|
texture_loader_basisu_failed(ld, &bld);
|
|
return;
|
|
}
|
|
|
|
uint num_load_images;
|
|
|
|
switch(file_info.tex_type) {
|
|
case BASIST_TYPE_2D:
|
|
num_load_images = 1;
|
|
|
|
if(file_info.total_images > num_load_images) {
|
|
log_warn("%s: Basis Universal texture contains more than 1 image; only image 0 will be used", ctx);
|
|
}
|
|
|
|
ld->params.class = TEXTURE_CLASS_2D;
|
|
break;
|
|
|
|
case BASIST_TYPE_CUBEMAP_ARRAY:
|
|
num_load_images = 6;
|
|
|
|
if(file_info.total_images < num_load_images) {
|
|
log_error("%s: Cubemap contains only %u faces; need 6",
|
|
ctx, file_info.total_images
|
|
);
|
|
texture_loader_basisu_failed(ld, &bld);
|
|
return;
|
|
}
|
|
|
|
if(file_info.total_images > num_load_images) {
|
|
log_warn("%s: Basis Universal cubemap contains more than 6 faces; only first 6 will be used", ctx);
|
|
}
|
|
|
|
ld->params.class = TEXTURE_CLASS_CUBEMAP;
|
|
break;
|
|
|
|
default:
|
|
log_error("%s: Unsupported Basis Universal texture type %s",
|
|
ctx, basist_get_texture_type_name(file_info.tex_type)
|
|
);
|
|
texture_loader_basisu_failed(ld, &bld);
|
|
return;
|
|
}
|
|
|
|
file_info.total_images = num_load_images;
|
|
|
|
uint32_t taisei_meta = file_info.userdata.userdata[0];
|
|
|
|
if((file_info.userdata.userdata[1] & 0xFFFFFF00) != BASISU_TAISEI_ID) {
|
|
log_warn("%s: No Taisei metadata in Basis Universal texture", ctx);
|
|
|
|
if(file_info.has_alpha_slices) {
|
|
taisei_meta = BASISU_TAISEI_CHANNELS_RGBA;
|
|
} else {
|
|
taisei_meta = BASISU_TAISEI_CHANNELS_RGB;
|
|
}
|
|
|
|
assert(0);
|
|
}
|
|
|
|
if(taisei_meta & BASISU_TAISEI_SRGB) {
|
|
ld->params.flags |= TEX_FLAG_SRGB;
|
|
} else {
|
|
ld->params.flags &= ~TEX_FLAG_SRGB;
|
|
}
|
|
|
|
bld.px_origin = file_info.y_flipped ? PIXMAP_ORIGIN_BOTTOMLEFT : PIXMAP_ORIGIN_TOPLEFT;
|
|
PixmapFormat px_fallback_format = PIXMAP_FORMAT_RGBA8;
|
|
basist_texture_format basis_fallback_format = BASIST_FORMAT_RGBA32;
|
|
|
|
TextureTypeQueryResult qr = { 0 };
|
|
|
|
PixmapFormat choosen_format = texture_loader_basisu_pick_and_apply_compressed_format(
|
|
ld,
|
|
taisei_meta,
|
|
file_info.source_format,
|
|
bld.px_origin,
|
|
ld->preferred_format ? ld->preferred_format : px_fallback_format,
|
|
&qr
|
|
);
|
|
|
|
if(!choosen_format) {
|
|
log_error("%s: Could not choose texture type", ctx);
|
|
texture_loader_basisu_failed(ld, &bld);
|
|
return;
|
|
}
|
|
|
|
basist_transcode_level_params p = { 0 };
|
|
basist_init_transcode_level_params(&p);
|
|
|
|
if(pixmap_format_is_compressed(choosen_format)) {
|
|
bld.px_decode_format = choosen_format;
|
|
bld.is_uncompressed_fallback = false;
|
|
p.format = compfmt_pixmap_to_basist(choosen_format);
|
|
} else {
|
|
bld.px_decode_format = px_fallback_format;
|
|
bld.is_uncompressed_fallback = true;
|
|
p.format = basis_fallback_format;
|
|
}
|
|
|
|
assume(file_info.total_images >= 1);
|
|
basist_image_info img_infos[file_info.total_images];
|
|
|
|
for(uint i = 0; i < ARRAY_SIZE(img_infos); ++i) {
|
|
TRY(basist_transcoder_get_image_info, bld.tc, i, img_infos + i);
|
|
}
|
|
|
|
TRY_SILENT(texture_loader_basisu_check_consistency, ctx, bld.tc, &file_info, img_infos);
|
|
TRY_SILENT(texture_loader_basisu_init_mipmaps, ctx, &bld, ld, &img_infos[0]);
|
|
texture_loader_basisu_set_swizzle(ld, bld.px_decode_format, taisei_meta);
|
|
|
|
bld.swizzle_supported = r_supports(RFEAT_TEXTURE_SWIZZLE);
|
|
bld.transcoding_started = false;
|
|
|
|
switch(ld->params.class) {
|
|
case TEXTURE_CLASS_2D: {
|
|
p.image_index = 0;
|
|
for(uint mip = 0; mip < ld->params.mipmaps; ++mip) {
|
|
p.level_index = mip + bld.mip_bias;
|
|
TRY_SILENT(texture_loader_basisu_load_pixmap, ctx, &bld, ld, &p, ld->pixmaps + mip);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case TEXTURE_CLASS_CUBEMAP:
|
|
for(int face = 0; face < 6; ++face) {
|
|
p.image_index = face;
|
|
for(uint mip = 0; mip < ld->params.mipmaps; ++mip) {
|
|
p.level_index = mip + bld.mip_bias;
|
|
Pixmap *px = &ld->cubemaps[mip].faces[face];
|
|
TRY_SILENT(texture_loader_basisu_load_pixmap, ctx, &bld, ld, &p, px);
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
default: UNREACHABLE;
|
|
}
|
|
|
|
if(bld.is_uncompressed_fallback && !bld.swizzle_supported) {
|
|
ld->params.swizzle = (SwizzleMask) { "rgba" };
|
|
}
|
|
|
|
TRY(basist_transcoder_stop_transcoding, bld.tc);
|
|
|
|
// These are expected to be pre-applied if needed
|
|
ld->preprocess.multiply_alpha = 0;
|
|
ld->preprocess.apply_alphamap = 0;
|
|
|
|
// Don't unset this one; it may be set in case we've fallen back to decompression
|
|
// and the renderer has no sRGB sampling support for this texture type.
|
|
// ld->preprocess.linearize = 0;
|
|
|
|
texture_loader_basisu_cleanup(&bld);
|
|
texture_loader_continue(ld);
|
|
}
|