vk: use imagelib cubemap/skybox loader

Engine imagelib already has skybox loader. It uses rgbdata_t
IMAGE_CUBEMAP flag.
1. Support IMAGE_CUBEMAP
2. Utilize imagelib skybox loader, do not try to load individual skybox
   sides manually.

This will allow loading KTX2 cubemaps directly.

Known issues:
1. Compressed KTX2 sides are not rotated correctly. Engine/imagelib is
   unable to rotate compressed images.
2. Some KTX2 sides are corrupted. Cause unknown.
This commit is contained in:
Ivan Avdeev 2023-12-14 13:40:34 -05:00
parent 6a11c6e64f
commit b1b333f74a
5 changed files with 116 additions and 113 deletions

View File

@ -95,6 +95,7 @@ static const loadpixformat_t load_null[] =
static const loadpixformat_t load_game[] =
{
{ "%s%s.%s", "ktx2", Image_LoadKTX2, IL_HINT_NO }, // ktx2 for world and studio models
{ "%s%s.%s", "dds", Image_LoadDDS, IL_HINT_NO }, // dds for world and studio models
{ "%s%s.%s", "bmp", Image_LoadBMP, IL_HINT_NO }, // WON menu images
{ "%s%s.%s", "tga", Image_LoadTGA, IL_HINT_NO }, // hl vgui menus
@ -105,7 +106,6 @@ static const loadpixformat_t load_game[] =
{ "%s%s.%s", "lmp", Image_LoadLMP, IL_HINT_NO }, // hl menu images (cached.wad etc)
{ "%s%s.%s", "fnt", Image_LoadFNT, IL_HINT_HL }, // hl console font (fonts.wad etc)
{ "%s%s.%s", "pal", Image_LoadPAL, IL_HINT_NO }, // install studio\sprite palette
{ "%s%s.%s", "ktx2", Image_LoadKTX2, IL_HINT_NO }, // ktx2 for world and studio models
{ NULL, NULL, NULL, IL_HINT_NO }
};

View File

@ -7,11 +7,14 @@
KTX2 sides should be pre-rotated
- [ ] KTX2 cubemaps
- [x] do not generate mips for skybox
- [x] support imagelib cubemaps
- [x] use imagelib skybox loader
- [ ] fix ktx2 sides corruption
- [x] Hide all SURF_DRAWSKY while retaining skybox, #579
- [ ] add skybox test
- [ ] possible issues with TF_NOMIPMAP
- [ ] used incorrectly when loading blue noise textures
- [ ] what about regular usage?
- [x] possible issues with TF_NOMIPMAP
- [x] used incorrectly when loading blue noise textures
- [x] what about regular usage?
# 2023-12-11 E345
- [x] fix black dielectrics, #666

View File

@ -232,20 +232,11 @@ static void createDefaultTextures( void )
tglob.cinTexture = R_TextureUploadFromBufferNew( "*cintexture", pic, TF_NOMIPMAP|TF_CLAMP );
{
rgbdata_t *sides[6];
pic = Common_FakeImage( 4, 4, 1, IMAGE_HAS_COLOR );
for( x = 0; x < 16; x++ )
((uint *)pic->buffer)[x] = 0;
sides[0] = pic;
sides[1] = pic;
sides[2] = pic;
sides[3] = pic;
sides[4] = pic;
sides[5] = pic;
pic = Common_FakeImage( 4, 4, 1, IMAGE_HAS_COLOR | IMAGE_CUBEMAP );
memset(pic->buffer, 0, pic->size);
const qboolean is_placeholder = true;
R_VkTexturesSkyboxUploadSides( "skybox_placeholder", sides, kColorspaceGamma, is_placeholder );
R_VkTexturesSkyboxUpload( "skybox_placeholder", pic, kColorspaceGamma, is_placeholder );
}
}
@ -477,50 +468,6 @@ void BuildMipMap( byte *in, int srcWidth, int srcHeight, int srcDepth, int flags
}
}
qboolean validatePicLayers(const char* const name, rgbdata_t *const *const layers, int num_layers) {
for (int i = 0; i < num_layers; ++i) {
// FIXME create empty black texture if there's no buffer
if (!layers[i]->buffer) {
ERR("Texture %s layer %d missing buffer", name, i);
return false;
}
if (i == 0)
continue;
if (layers[0]->type != layers[i]->type) {
ERR("Texture %s layer %d has type %d inconsistent with layer 0 type %d", name, i, layers[i]->type, layers[0]->type);
return false;
}
if (layers[0]->width != layers[i]->width
|| layers[0]->height != layers[i]->height
|| layers[0]->depth != layers[i]->depth) {
ERR("Texture %s layer %d has resolution %dx%d%d inconsistent with layer 0 resolution %dx%dx%d",
name, i,
layers[i]->width, layers[i]->height, layers[i]->depth,
layers[0]->width, layers[0]->height, layers[0]->depth);
return false;
}
if ((layers[0]->flags ^ layers[i]->flags) & IMAGE_HAS_ALPHA) {
ERR("Texture %s layer %d has_alpha=%d inconsistent with layer 0 has_alpha=%d",
name, i,
!!(layers[i]->flags & IMAGE_HAS_ALPHA),
!!(layers[0]->flags & IMAGE_HAS_ALPHA));
return false;
}
if (layers[0]->numMips != layers[i]->numMips) {
ERR("Texture %s layer %d has numMips %d inconsistent with layer 0 numMips %d",
name, i, layers[i]->numMips, layers[0]->numMips);
return false;
}
}
return true;
}
///////////// Render API funcs /////////////
int R_TextureFindByName( const char *name )
{
@ -584,11 +531,16 @@ static int loadTextureInternalFromFile( const char *name, const byte *buf, size_
if( !pic )
goto cleanup;
if (pic->flags & IMAGE_CUBEMAP) {
ERR("%s: '%s' is invalid: cubemaps are not supported here", __FUNCTION__, name);
goto cleanup;
}
// Process flags, convert to rgba, etc
tex->flags = flags;
ProcessImage( tex, pic );
if( !R_VkTextureUpload( insert.index, tex, &pic, 1, colorspace_hint ))
if( !R_VkTextureUpload( insert.index, tex, pic, colorspace_hint ))
goto cleanup;
// New textures should have refcount = 1 regardless of refcount-aware calls
@ -708,6 +660,11 @@ int R_TextureUploadFromBuffer( const char *name, rgbdata_t *pic, texFlags_t flag
if( !pic )
return 0;
if (pic->flags & IMAGE_CUBEMAP) {
ERR("%s: '%s' is invalid: cubemaps are not supported here", __FUNCTION__, name);
return 0;
}
if( !checkTextureName( name ))
return 0;
@ -740,7 +697,7 @@ int R_TextureUploadFromBuffer( const char *name, rgbdata_t *pic, texFlags_t flag
ProcessImage( tex, pic );
if( !R_VkTextureUpload( insert.index, tex, &pic, 1, kColorspaceGamma ))
if( !R_VkTextureUpload( insert.index, tex, pic, kColorspaceGamma ))
{
if ( !update_only && insert.created )
urmomRemoveByIndex(&g_textures.all_desc, insert.index);
@ -757,6 +714,7 @@ int R_TextureUploadFromBuffer( const char *name, rgbdata_t *pic, texFlags_t flag
return insert.index;
}
/*
static const struct {
const char *suffix;
uint flags;
@ -842,7 +800,7 @@ static qboolean loadSkyboxSides( const char *prefix, int style ) {
{
const qboolean is_placeholder = false;
success = R_VkTexturesSkyboxUploadSides( prefix, sides, kColorspaceGamma, is_placeholder );
success = R_VkTexturesSkyboxUpload( prefix, sides, kColorspaceGamma, is_placeholder );
}
cleanup:
@ -874,6 +832,44 @@ static qboolean skyboxLoadSides(const_string_view_t base, const char *prefix) {
const int style = CheckSkyboxSides( loadname );
return loadSkyboxSides(loadname, style);
}
*/
static qboolean skyboxLoad(const_string_view_t base, const char *prefix) {
qboolean success = false;
char loadname[MAX_STRING];
Q_snprintf( loadname, sizeof( loadname ), prefix, base.len, base.s );
rgbdata_t *pic = gEngine.FS_LoadImage( loadname, NULL, 0 );
if (!pic)
return false;
if (!(pic->flags & IMAGE_CUBEMAP)) {
ERR("%s: '%s' is invalid: skybox is expected to be a cubemap ", __FUNCTION__, loadname);
goto cleanup;
}
{
uint img_flags = pic->flags;
if( pic->type == PF_INDEXED_24 || pic->type == PF_INDEXED_32 )
img_flags |= IMAGE_FORCE_RGBA;
gEngine.Image_Process( &pic, 0, 0, img_flags, 0.f );
}
{
const qboolean is_placeholder = false;
success = R_VkTexturesSkyboxUpload( prefix, pic, kColorspaceGamma, is_placeholder );
}
cleanup:
if (pic)
gEngine.FS_FreeImage(pic);
if (success)
DEBUG( "Loaded skybox %s", prefix );
return success;
}
static void skyboxUnload( void ) {
R_VkTexturesSkyboxUnload();
@ -915,11 +911,11 @@ static qboolean skyboxTryLoad( const char *skyboxname, qboolean force_reload ) {
}*/
// 2. pbr/env/<sky>_<side>.{png, ...} sides
if (skyboxLoadSides(basename, "pbr/env/%.*s"))
if (skyboxLoad(basename, "pbr/env/%.*s"))
goto success;
// 3. Old gfx/env/<sky>_<side>.{png, ...} sides
if (skyboxLoadSides(basename, "gfx/env/%.*s"))
if (skyboxLoad(basename, "gfx/env/%.*s"))
goto success;
return false;
@ -931,7 +927,7 @@ success:
static const char *k_skybox_default = "desert";
void skyboxLoad( const char *skyboxname, qboolean is_custom, qboolean force_reload ) {
void skyboxSetup( const char *skyboxname, qboolean is_custom, qboolean force_reload ) {
DEBUG("%s: skyboxname='%s' is_custom=%d force_reload=%d", __FUNCTION__, skyboxname, is_custom, force_reload);
if (!skyboxTryLoad(skyboxname, force_reload)) {
@ -949,12 +945,12 @@ void skyboxLoad( const char *skyboxname, qboolean is_custom, qboolean force_relo
void R_TextureSetupCustomSky( const char *skyboxname ) {
const qboolean is_custom = true;
const qboolean force_reload = false;
skyboxLoad(skyboxname, is_custom, force_reload);
skyboxSetup(skyboxname, is_custom, force_reload);
}
void R_TextureSetupSky( const char *skyboxname, qboolean force_reload ) {
const qboolean is_custom = false;
skyboxLoad(skyboxname, is_custom, force_reload);
skyboxSetup(skyboxname, is_custom, force_reload);
}
// FIXME move to r_textures_extra.h

View File

@ -52,17 +52,16 @@ static struct {
// Exported from r_textures.h
size_t CalcImageSize( pixformat_t format, int width, int height, int depth );
int CalcMipmapCount( int width, int height, int depth, uint32_t flags, qboolean haveBuffer );
qboolean validatePicLayers(const char* const name, rgbdata_t *const *const layers, int num_layers);
void BuildMipMap( byte *in, int srcWidth, int srcHeight, int srcDepth, int flags );
static VkSampler pickSamplerForFlags( texFlags_t flags );
static qboolean uploadTexture(int index, vk_texture_t *tex, rgbdata_t *const *const layers, int num_layers, qboolean cubemap, colorspace_hint_e colorspace_hint);
static qboolean uploadTexture(int index, vk_texture_t *tex, const rgbdata_t *layers, colorspace_hint_e colorspace_hint);
// Hardcode blue noise texture size to 64x64x64
#define BLUE_NOISE_SIZE 64
#define BLUE_NOISE_NAME_F "bluenoise/LDR_RGBA_%d.png"
static void generateFallbackNoiseTextures( rgbdata_t *pic ) {
static void generateFallbackNoiseTextures( const rgbdata_t *pic ) {
ERR("Generating bad quality regular noise textures as a fallback for blue noise textures");
const int blue_noise_count = pic->size / sizeof(uint32_t);
@ -80,11 +79,11 @@ static void loadBlueNoiseTextures(void) {
const size_t blue_noise_count = BLUE_NOISE_SIZE * BLUE_NOISE_SIZE * BLUE_NOISE_SIZE;
const size_t blue_noise_size = blue_noise_count * sizeof(uint32_t);
uint32_t *const scratch = Mem_Malloc(vk_core.pool /* TODO textures pool */, blue_noise_size);
rgbdata_t pic = {
const rgbdata_t pic = {
.width = BLUE_NOISE_SIZE,
.height = BLUE_NOISE_SIZE,
.depth = BLUE_NOISE_SIZE,
.flags = TF_NOMIPMAP,
.flags = 0,
.type = PF_RGBA_32,
.size = blue_noise_size,
.buffer = (byte*)scratch,
@ -136,9 +135,8 @@ static void loadBlueNoiseTextures(void) {
const char *const name = fail ? "*bluenoise/pcg_fallback" : "*bluenoise";
Q_strncpy(g_vktextures.blue_noise.hdr_.key, name, sizeof(g_vktextures.blue_noise.hdr_.key));
rgbdata_t *pica[1] = {&pic};
const qboolean is_cubemap = false;
ASSERT(uploadTexture(-1, &g_vktextures.blue_noise, pica, 1, is_cubemap, kColorspaceLinear));
g_vktextures.blue_noise.flags = TF_NOMIPMAP;
ASSERT(uploadTexture(-1, &g_vktextures.blue_noise, &pic, kColorspaceLinear));
Mem_Free(scratch);
}
@ -501,36 +499,44 @@ static const char* getColorspaceHintName(colorspace_hint_e ch) {
return "INVALID";
}
static qboolean uploadTexture(int index, vk_texture_t *tex, rgbdata_t *const *const layers, int num_layers, qboolean cubemap, colorspace_hint_e colorspace_hint) {
// xash imagelib cubemap layer order is not the one that vulkan expects
static const int g_remap_cube_layer[6] = {
/* ft = */ 3,
/* bk = */ 2,
/* up = */ 4,
/* dn = */ 5,
/* rt = */ 0,
/* lf = */ 1,
};
static qboolean uploadTexture(int index, vk_texture_t *tex, const rgbdata_t *pic, colorspace_hint_e colorspace_hint) {
tex->total_size = 0;
if (num_layers == 1 && layers[0]->type == PF_KTX2_RAW) {
if (!uploadRawKtx2(index, tex, layers[0]))
if (pic->type == PF_KTX2_RAW) {
if (!uploadRawKtx2(index, tex, pic))
return false;
} else {
const int width = layers[0]->width;
const int height = layers[0]->height;
const int depth = Q_max(1, layers[0]->depth);
const qboolean compute_mips = layers[0]->type == PF_RGBA_32 && layers[0]->numMips < 2;
const VkFormat format = VK_GetFormat(layers[0]->type, colorspace_hint);
const int mipCount = compute_mips ? CalcMipmapCount( width, height, depth, tex->flags, true ) : layers[0]->numMips;
const int width = pic->width;
const int height = pic->height;
const int depth = Q_max(1, pic->depth);
const qboolean compute_mips = !(tex->flags & TF_NOMIPMAP) && pic->type == PF_RGBA_32 && pic->numMips < 2;
const VkFormat format = VK_GetFormat(pic->type, colorspace_hint);
const int mipCount = compute_mips ? CalcMipmapCount( width, height, depth, tex->flags, true ) : Q_max(1, pic->numMips);
const qboolean is_cubemap = !!(pic->flags & IMAGE_CUBEMAP);
if (format == VK_FORMAT_UNDEFINED) {
ERR("Unsupported PF format %d", layers[0]->type);
ERR("Unsupported PF format %d", pic->type);
return false;
}
if (!validatePicLayers(TEX_NAME(tex), layers, num_layers))
return false;
DEBUG("Uploading texture[%d] %s, %dx%d fmt=%s(%s) cs=%s mips=%d(build=%d), layers=%d",
DEBUG("Uploading texture[%d] %s, %dx%d fmt=%s(%s) cs=%s mips=%d(build=%d), is_cubemap=%d",
index, TEX_NAME(tex), width, height,
getPFName(layers[0]->type), R_VkFormatName(format),
getPFName(pic->type), R_VkFormatName(format),
getColorspaceHintName(colorspace_hint),
mipCount, compute_mips, num_layers);
mipCount, compute_mips, is_cubemap);
// TODO (not sure why, but GL does this)
// if( !ImageCompressed( layers->type ) && !FBitSet( tex->flags, TF_NOMIPMAP ) && FBitSet( layers->flags, IMAGE_ONEBIT_ALPHA ))
// if( !ImageCompressed( pic->type ) && !FBitSet( tex->flags, TF_NOMIPMAP ) && FBitSet( pic->flags, IMAGE_ONEBIT_ALPHA ))
// data = GL_ApplyFilter( data, tex->width, tex->height );
{
@ -540,13 +546,13 @@ static qboolean uploadTexture(int index, vk_texture_t *tex, rgbdata_t *const *co
.height = height,
.depth = depth,
.mips = mipCount,
.layers = num_layers,
.layers = is_cubemap ? 6 : 1,
.format = format,
.tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
.flags = 0
| ((layers[0]->flags & IMAGE_HAS_ALPHA) ? 0 : kVkImageFlagIgnoreAlpha)
| (cubemap ? kVkImageFlagIsCubemap : 0)
| ((pic->flags & IMAGE_HAS_ALPHA) ? 0 : kVkImageFlagIgnoreAlpha)
| (is_cubemap ? kVkImageFlagIsCubemap : 0)
| (colorspace_hint == kColorspaceGamma ? kVkImageFlagCreateUnormView : 0),
};
@ -561,17 +567,16 @@ static qboolean uploadTexture(int index, vk_texture_t *tex, rgbdata_t *const *co
{
R_VkImageUploadBegin(&tex->vk.image);
for (int layer = 0; layer < num_layers; ++layer) {
const rgbdata_t *const pic = layers[layer];
byte *buf = pic->buffer;
byte *buf = pic->buffer;
const int layers_count = is_cubemap ? 6 : 1;
for (int layer = 0; layer < layers_count; ++layer) {
for (int mip = 0; mip < mipCount; ++mip) {
const int width = Q_max( 1, ( pic->width >> mip ));
const int height = Q_max( 1, ( pic->height >> mip ));
const int depth = Q_max( 1, ( pic->depth >> mip ));
const size_t mip_size = CalcImageSize( pic->type, width, height, depth );
R_VkImageUploadSlice(&tex->vk.image, layer, mip, mip_size, buf);
R_VkImageUploadSlice(&tex->vk.image, is_cubemap ? g_remap_cube_layer[layer] : 0, mip, mip_size, buf);
tex->total_size += mip_size;
// Build mip in place for the next mip level
@ -582,6 +587,10 @@ static qboolean uploadTexture(int index, vk_texture_t *tex, rgbdata_t *const *co
buf += mip_size;
}
}
if (compute_mips) {
buf += CalcImageSize(pic->type, width, height, depth);
}
}
R_VkImageUploadEnd(&tex->vk.image);
@ -595,8 +604,8 @@ static qboolean uploadTexture(int index, vk_texture_t *tex, rgbdata_t *const *co
return true;
}
qboolean R_VkTextureUpload(int index, vk_texture_t *tex, rgbdata_t *const *const layers, int num_layers, colorspace_hint_e colorspace_hint) {
return uploadTexture( index, tex, layers, num_layers, false, colorspace_hint );
qboolean R_VkTextureUpload(int index, vk_texture_t *tex, const rgbdata_t *pic, colorspace_hint_e colorspace_hint) {
return uploadTexture( index, tex, pic, colorspace_hint );
}
void R_VkTextureDestroy( int index, vk_texture_t *tex ) {
@ -638,16 +647,12 @@ VkDescriptorImageInfo R_VkTexturesGetSkyboxDescriptorImageInfo( void ) {
};
}
qboolean R_VkTexturesSkyboxUploadSides( const char *name, rgbdata_t *const sides[6], colorspace_hint_e colorspace_hint, qboolean placeholder) {
qboolean R_VkTexturesSkyboxUpload( const char *name, const rgbdata_t *pic, colorspace_hint_e colorspace_hint, qboolean placeholder) {
vk_texture_t *const dest = placeholder ? &g_vktextures.cubemap_placeholder : &g_vktextures.skybox_cube;
Q_strncpy( TEX_NAME(dest), name, sizeof( TEX_NAME(dest) ));
dest->flags |= TF_NOMIPMAP;
return uploadTexture(-1, dest, sides, 6, true, colorspace_hint);
}
qboolean R_VkTexturesSkyboxUpload( const char *name, rgbdata_t *const pic ) {
ERR("%s(%s): not implemented", __FUNCTION__, name);
return false;
ASSERT(pic->flags & IMAGE_CUBEMAP);
return uploadTexture(-1, dest, pic, colorspace_hint);
}
VkDescriptorSet R_VkTextureGetDescriptorUnorm( uint index ) {

View File

@ -34,11 +34,10 @@ typedef struct vk_texture_s
qboolean R_VkTexturesInit( void );
void R_VkTexturesShutdown( void );
qboolean R_VkTexturesSkyboxUploadSides( const char *name, rgbdata_t *const sides[6], colorspace_hint_e colorspace_hint, qboolean placeholder);
qboolean R_VkTexturesSkyboxUpload( const char *name, rgbdata_t *const pic );
qboolean R_VkTexturesSkyboxUpload( const char *name, const rgbdata_t *pic, colorspace_hint_e colorspace_hint, qboolean placeholder);
void R_VkTexturesSkyboxUnload(void);
qboolean R_VkTextureUpload(int index, vk_texture_t *tex, rgbdata_t *const *const layers, int num_layers, colorspace_hint_e colorspace_hint);
qboolean R_VkTextureUpload(int index, vk_texture_t *tex, const rgbdata_t *pic, colorspace_hint_e colorspace_hint);
void R_VkTextureDestroy(int index, vk_texture_t *tex);
VkDescriptorImageInfo R_VkTexturesGetSkyboxDescriptorImageInfo( void );