Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #12102 from iwubcode/cubemap_custom_texture
VideoCommon: add ability to load cube maps into custom texture data
  • Loading branch information
JMC47 committed Sep 3, 2023
2 parents 6f6eb73 + 62fee2f commit 900439e
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 117 deletions.
90 changes: 50 additions & 40 deletions Source/Core/VideoCommon/Assets/CustomAssetLibrary.cpp
Expand Up @@ -15,9 +15,12 @@ namespace
std::size_t GetAssetSize(const CustomTextureData& data)
{
std::size_t total = 0;
for (const auto& level : data.m_levels)
for (const auto& slice : data.m_slices)
{
total += level.data.size();
for (const auto& level : slice.m_levels)
{
total += level.data.size();
}
}
return total;
}
Expand All @@ -30,51 +33,58 @@ CustomAssetLibrary::LoadInfo CustomAssetLibrary::LoadGameTexture(const AssetID&
return {};

// Note: 'LoadTexture()' ensures we have a level loaded
const auto& first_mip = data->m_levels[0];

// Verify that each mip level is the correct size (divide by 2 each time).
u32 current_mip_width = first_mip.width;
u32 current_mip_height = first_mip.height;
for (u32 mip_level = 1; mip_level < static_cast<u32>(data->m_levels.size()); mip_level++)
for (std::size_t slice_index = 0; slice_index < data->m_slices.size(); slice_index++)
{
if (current_mip_width != 1 || current_mip_height != 1)
auto& slice = data->m_slices[slice_index];
const auto& first_mip = slice.m_levels[0];

// Verify that each mip level is the correct size (divide by 2 each time).
u32 current_mip_width = first_mip.width;
u32 current_mip_height = first_mip.height;
for (u32 mip_level = 1; mip_level < static_cast<u32>(slice.m_levels.size()); mip_level++)
{
current_mip_width = std::max(current_mip_width / 2, 1u);
current_mip_height = std::max(current_mip_height / 2, 1u);
if (current_mip_width != 1 || current_mip_height != 1)
{
current_mip_width = std::max(current_mip_width / 2, 1u);
current_mip_height = std::max(current_mip_height / 2, 1u);

const VideoCommon::CustomTextureData::Level& level = data->m_levels[mip_level];
if (current_mip_width == level.width && current_mip_height == level.height)
continue;
const VideoCommon::CustomTextureData::ArraySlice::Level& level = slice.m_levels[mip_level];
if (current_mip_width == level.width && current_mip_height == level.height)
continue;

ERROR_LOG_FMT(VIDEO,
"Invalid custom game texture size {}x{} for texture asset {}. Mipmap level {} "
"must be {}x{}.",
level.width, level.height, asset_id, mip_level, current_mip_width,
current_mip_height);
}
else
{
// It is invalid to have more than a single 1x1 mipmap.
ERROR_LOG_FMT(VIDEO,
"Custom game texture {} has too many 1x1 mipmaps. Skipping extra levels.",
asset_id);
}
ERROR_LOG_FMT(VIDEO,
"Invalid custom game texture size {}x{} for texture asset {}. Slice {} with "
"mipmap level {} "
"must be {}x{}.",
level.width, level.height, asset_id, slice_index, mip_level,
current_mip_width, current_mip_height);
}
else
{
// It is invalid to have more than a single 1x1 mipmap.
ERROR_LOG_FMT(
VIDEO,
"Custom game texture {} has too many 1x1 mipmaps for slice {}. Skipping extra levels.",
asset_id, slice_index);
}

// Drop this mip level and any others after it.
while (data->m_levels.size() > mip_level)
data->m_levels.pop_back();
}
// Drop this mip level and any others after it.
while (slice.m_levels.size() > mip_level)
slice.m_levels.pop_back();
}

// All levels have to have the same format.
if (std::any_of(data->m_levels.begin(), data->m_levels.end(),
[&first_mip](const VideoCommon::CustomTextureData::Level& l) {
return l.format != first_mip.format;
}))
{
ERROR_LOG_FMT(VIDEO, "Custom game texture {} has inconsistent formats across mip levels.",
asset_id);
// All levels have to have the same format.
if (std::any_of(slice.m_levels.begin(), slice.m_levels.end(),
[&first_mip](const VideoCommon::CustomTextureData::ArraySlice::Level& l) {
return l.format != first_mip.format;
}))
{
ERROR_LOG_FMT(
VIDEO, "Custom game texture {} has inconsistent formats across mip levels for slice {}.",
asset_id, slice_index);

return {};
return {};
}
}

return load_info;
Expand Down
131 changes: 90 additions & 41 deletions Source/Core/VideoCommon/Assets/CustomTextureData.cpp
Expand Up @@ -62,6 +62,19 @@ struct DDS_PIXELFORMAT
#define DDS_PAL8A 0x00000021 // DDPF_PALETTEINDEXED8 | DDPF_ALPHAPIXELS
#define DDS_BUMPDUDV 0x00080000 // DDPF_BUMPDUDV

#define DDS_CUBEMAP_POSITIVEX 0x00000600 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX
#define DDS_CUBEMAP_NEGATIVEX 0x00000a00 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEX
#define DDS_CUBEMAP_POSITIVEY 0x00001200 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEY
#define DDS_CUBEMAP_NEGATIVEY 0x00002200 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEY
#define DDS_CUBEMAP_POSITIVEZ 0x00004200 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ
#define DDS_CUBEMAP_NEGATIVEZ 0x00008200 // DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ

#define DDS_CUBEMAP_ALLFACES \
(DDS_CUBEMAP_POSITIVEX | DDS_CUBEMAP_NEGATIVEX | DDS_CUBEMAP_POSITIVEY | DDS_CUBEMAP_NEGATIVEY | \
DDS_CUBEMAP_POSITIVEZ | DDS_CUBEMAP_NEGATIVEZ)

#define DDS_CUBEMAP 0x00000200 // DDSCAPS2_CUBEMAP

#ifndef MAKEFOURCC
#define MAKEFOURCC(ch0, ch1, ch2, ch3) \
((uint32_t)(uint8_t)(ch0) | ((uint32_t)(uint8_t)(ch1) << 8) | ((uint32_t)(uint8_t)(ch2) << 16) | \
Expand Down Expand Up @@ -143,12 +156,13 @@ struct DDSLoadInfo
u32 width = 0;
u32 height = 0;
u32 mip_count = 0;
u32 array_size = 0;
AbstractTextureFormat format = AbstractTextureFormat::RGBA8;
size_t first_mip_offset = 0;
size_t first_mip_size = 0;
u32 first_mip_row_length = 0;

std::function<void(VideoCommon::CustomTextureData::Level*)> conversion_function;
std::function<void(VideoCommon::CustomTextureData::ArraySlice::Level*)> conversion_function;
};

static constexpr u32 GetBlockCount(u32 extent, u32 block_size)
Expand All @@ -171,7 +185,7 @@ static u32 CalculateMipCount(u32 width, u32 height)
return mip_count;
}

static void ConvertTexture_X8B8G8R8(VideoCommon::CustomTextureData::Level* level)
static void ConvertTexture_X8B8G8R8(VideoCommon::CustomTextureData::ArraySlice::Level* level)
{
u8* data_ptr = level->data.data();
for (u32 row = 0; row < level->height; row++)
Expand All @@ -185,7 +199,7 @@ static void ConvertTexture_X8B8G8R8(VideoCommon::CustomTextureData::Level* level
}
}

static void ConvertTexture_A8R8G8B8(VideoCommon::CustomTextureData::Level* level)
static void ConvertTexture_A8R8G8B8(VideoCommon::CustomTextureData::ArraySlice::Level* level)
{
u8* data_ptr = level->data.data();
for (u32 row = 0; row < level->height; row++)
Expand All @@ -202,7 +216,7 @@ static void ConvertTexture_A8R8G8B8(VideoCommon::CustomTextureData::Level* level
}
}

static void ConvertTexture_X8R8G8B8(VideoCommon::CustomTextureData::Level* level)
static void ConvertTexture_X8R8G8B8(VideoCommon::CustomTextureData::ArraySlice::Level* level)
{
u8* data_ptr = level->data.data();
for (u32 row = 0; row < level->height; row++)
Expand All @@ -219,7 +233,7 @@ static void ConvertTexture_X8R8G8B8(VideoCommon::CustomTextureData::Level* level
}
}

static void ConvertTexture_R8G8B8(VideoCommon::CustomTextureData::Level* level)
static void ConvertTexture_R8G8B8(VideoCommon::CustomTextureData::ArraySlice::Level* level)
{
std::vector<u8> new_data(level->row_length * level->height * sizeof(u32));

Expand Down Expand Up @@ -297,13 +311,26 @@ static bool ParseDDSHeader(File::IOFile& file, DDSLoadInfo* info)
if (!file.ReadBytes(&dxt10_header, sizeof(dxt10_header)))
return false;

// Can't handle array textures here. Doesn't make sense to use them, anyway.
if (dxt10_header.resourceDimension != DDS_DIMENSION_TEXTURE2D || dxt10_header.arraySize != 1)
return false;

info->array_size = dxt10_header.arraySize;
header_size += sizeof(dxt10_header);
dxt10_format = dxt10_header.dxgiFormat;
}
else
{
if (header.dwCaps2 & DDS_CUBEMAP)
{
if ((header.dwCaps2 & DDS_CUBEMAP_ALLFACES) != DDS_CUBEMAP_ALLFACES)
{
return false;
}

info->array_size = 6;
}
else
{
info->array_size = 1;
}
}

// Currently, we only handle compressed textures here, and leave the rest to the SOIL loader.
// In the future, this could be extended, but these isn't much benefit in doing so currently.
Expand Down Expand Up @@ -369,6 +396,20 @@ static bool ParseDDSHeader(File::IOFile& file, DDSLoadInfo* info)
return false;
}

if (header.dwCaps2 & DDS_CUBEMAP)
{
if ((header.dwCaps2 & DDS_CUBEMAP_ALLFACES) != DDS_CUBEMAP_ALLFACES)
{
return false;
}

info->array_size = 6;
}
else
{
info->array_size = 1;
}

// All these formats are RGBA, just with byte swapping.
info->format = AbstractTextureFormat::RGBA8;
info->block_size = 1;
Expand Down Expand Up @@ -416,9 +457,10 @@ static bool ParseDDSHeader(File::IOFile& file, DDSLoadInfo* info)
return true;
}

static bool ReadMipLevel(VideoCommon::CustomTextureData::Level* level, File::IOFile& file,
const std::string& filename, u32 mip_level, const DDSLoadInfo& info,
u32 width, u32 height, u32 row_length, size_t size)
static bool ReadMipLevel(VideoCommon::CustomTextureData::ArraySlice::Level* level,
File::IOFile& file, const std::string& filename, u32 mip_level,
const DDSLoadInfo& info, u32 width, u32 height, u32 row_length,
size_t size)
{
// D3D11 cannot handle block compressed textures where the first mip level is
// not a multiple of the block size.
Expand Down Expand Up @@ -463,43 +505,50 @@ bool LoadDDSTexture(CustomTextureData* texture, const std::string& filename)
if (!ParseDDSHeader(file, &info))
return false;

// Read first mip level, as it may have a custom pitch.
CustomTextureData::Level first_level;
if (!file.Seek(info.first_mip_offset, File::SeekOrigin::Begin) ||
!ReadMipLevel(&first_level, file, filename, 0, info, info.width, info.height,
info.first_mip_row_length, info.first_mip_size))
{
if (!file.Seek(info.first_mip_offset, File::SeekOrigin::Begin))
return false;
}

texture->m_levels.push_back(std::move(first_level));

// Read in any remaining mip levels in the file.
// If the .dds file does not contain a full mip chain, we'll fall back to the old path.
u32 mip_width = info.width;
u32 mip_height = info.height;
for (u32 i = 1; i < info.mip_count; i++)
for (u32 arr_i = 0; arr_i < info.array_size; arr_i++)
{
mip_width = std::max(mip_width / 2, 1u);
mip_height = std::max(mip_height / 2, 1u);
auto& slice = texture->m_slices.emplace_back();
// Read first mip level, as it may have a custom pitch.
CustomTextureData::ArraySlice::Level first_level;
if (!ReadMipLevel(&first_level, file, filename, 0, info, info.width, info.height,
info.first_mip_row_length, info.first_mip_size))
{
return false;
}

// Pitch can't be specified with each mip level, so we have to calculate it ourselves.
u32 blocks_wide = GetBlockCount(mip_width, info.block_size);
u32 blocks_high = GetBlockCount(mip_height, info.block_size);
u32 mip_row_length = blocks_wide * info.block_size;
size_t mip_size = blocks_wide * static_cast<size_t>(info.bytes_per_block) * blocks_high;
CustomTextureData::Level level;
if (!ReadMipLevel(&level, file, filename, i, info, mip_width, mip_height, mip_row_length,
mip_size))
break;

texture->m_levels.push_back(std::move(level));
slice.m_levels.push_back(std::move(first_level));

// Read in any remaining mip levels in the file.
// If the .dds file does not contain a full mip chain, we'll fall back to the old path.
u32 mip_width = info.width;
u32 mip_height = info.height;
for (u32 i = 1; i < info.mip_count; i++)
{
mip_width = std::max(mip_width / 2, 1u);
mip_height = std::max(mip_height / 2, 1u);

// Pitch can't be specified with each mip level, so we have to calculate it ourselves.
u32 blocks_wide = GetBlockCount(mip_width, info.block_size);
u32 blocks_high = GetBlockCount(mip_height, info.block_size);
u32 mip_row_length = blocks_wide * info.block_size;
size_t mip_size = blocks_wide * static_cast<size_t>(info.bytes_per_block) * blocks_high;
CustomTextureData::ArraySlice::Level level;
if (!ReadMipLevel(&level, file, filename, i, info, mip_width, mip_height, mip_row_length,
mip_size))
break;

slice.m_levels.push_back(std::move(level));
}
}

return true;
}

bool LoadDDSTexture(CustomTextureData::Level* level, const std::string& filename, u32 mip_level)
bool LoadDDSTexture(CustomTextureData::ArraySlice::Level* level, const std::string& filename,
u32 mip_level)
{
// Only loading a single mip level.
File::IOFile file;
Expand All @@ -515,7 +564,7 @@ bool LoadDDSTexture(CustomTextureData::Level* level, const std::string& filename
info.first_mip_row_length, info.first_mip_size);
}

bool LoadPNGTexture(CustomTextureData::Level* level, const std::string& filename)
bool LoadPNGTexture(CustomTextureData::ArraySlice::Level* level, const std::string& filename)
{
if (!level) [[unlikely]]
return false;
Expand Down
23 changes: 14 additions & 9 deletions Source/Core/VideoCommon/Assets/CustomTextureData.h
Expand Up @@ -14,18 +14,23 @@ namespace VideoCommon
class CustomTextureData
{
public:
struct Level
struct ArraySlice
{
std::vector<u8> data;
AbstractTextureFormat format = AbstractTextureFormat::RGBA8;
u32 width = 0;
u32 height = 0;
u32 row_length = 0;
struct Level
{
std::vector<u8> data;
AbstractTextureFormat format = AbstractTextureFormat::RGBA8;
u32 width = 0;
u32 height = 0;
u32 row_length = 0;
};
std::vector<Level> m_levels;
};
std::vector<Level> m_levels;
std::vector<ArraySlice> m_slices;
};

bool LoadDDSTexture(CustomTextureData* texture, const std::string& filename);
bool LoadDDSTexture(CustomTextureData::Level* level, const std::string& filename, u32 mip_level);
bool LoadPNGTexture(CustomTextureData::Level* level, const std::string& filename);
bool LoadDDSTexture(CustomTextureData::ArraySlice::Level* level, const std::string& filename,
u32 mip_level);
bool LoadPNGTexture(CustomTextureData::ArraySlice::Level* level, const std::string& filename);
} // namespace VideoCommon

0 comments on commit 900439e

Please sign in to comment.