Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VideoCommon: add ability to load cube maps into custom texture data #12102

Merged
merged 1 commit into from
Sep 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
90 changes: 50 additions & 40 deletions Source/Core/VideoCommon/Assets/CustomAssetLibrary.cpp
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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