Merge pull request #12102 from iwubcode/cubemap_custom_texture

VideoCommon: add ability to load cube maps into custom texture data
This commit is contained in:
JMC47 2023-09-03 18:29:00 -04:00 committed by GitHub
commit 900439ea0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 225 additions and 118 deletions

View File

@ -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;
}
@ -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)
{
current_mip_width = std::max(current_mip_width / 2, 1u);
current_mip_height = std::max(current_mip_height / 2, 1u);
auto& slice = data->m_slices[slice_index];
const auto& first_mip = slice.m_levels[0];
const VideoCommon::CustomTextureData::Level& level = data->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
// 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++)
{
// 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);
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::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 {}. 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 (slice.m_levels.size() > mip_level)
slice.m_levels.pop_back();
}
// Drop this mip level and any others after it.
while (data->m_levels.size() > mip_level)
data->m_levels.pop_back();
}
// 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);
// 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);
return {};
return {};
}
}
return load_info;

View File

@ -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) | \
@ -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)
@ -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++)
@ -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++)
@ -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++)
@ -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));
@ -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.
@ -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;
@ -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.
@ -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;
slice.m_levels.push_back(std::move(first_level));
texture->m_levels.push_back(std::move(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;
@ -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;

View File

@ -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

View File

@ -9,7 +9,6 @@
#include "Common/FileUtil.h"
#include "Common/Logging/Log.h"
#include "Common/StringUtil.h"
#include "VideoCommon/Assets/CustomTextureData.h"
#include "VideoCommon/Assets/MaterialAsset.h"
#include "VideoCommon/Assets/ShaderAsset.h"
@ -34,9 +33,12 @@ std::chrono::system_clock::time_point FileTimeToSysTime(std::filesystem::file_ti
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;
}
@ -247,24 +249,32 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const Ass
return {};
}
if (!LoadMips(asset_path, data))
if (data->m_slices.empty()) [[unlikely]]
data->m_slices.push_back({});
if (!LoadMips(asset_path, &data->m_slices[0]))
return {};
return LoadInfo{GetAssetSize(*data), FileTimeToSysTime(last_loaded_time)};
}
else if (ext == ".png")
{
// If we have no levels, create one to pass into LoadPNGTexture
if (data->m_levels.empty())
data->m_levels.push_back({});
// If we have no slices, create one
if (data->m_slices.empty())
data->m_slices.push_back({});
if (!LoadPNGTexture(&data->m_levels[0], PathToString(asset_path)))
auto& slice = data->m_slices[0];
// If we have no levels, create one to pass into LoadPNGTexture
if (slice.m_levels.empty())
slice.m_levels.push_back({});
if (!LoadPNGTexture(&slice.m_levels[0], PathToString(asset_path)))
{
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - could not load png texture!", asset_id);
return {};
}
if (!LoadMips(asset_path, data))
if (!LoadMips(asset_path, &slice))
return {};
return LoadInfo{GetAssetSize(*data), FileTimeToSysTime(last_loaded_time)};
@ -282,7 +292,7 @@ void DirectFilesystemAssetLibrary::SetAssetIDMapData(const AssetID& asset_id,
}
bool DirectFilesystemAssetLibrary::LoadMips(const std::filesystem::path& asset_path,
CustomTextureData* data)
CustomTextureData::ArraySlice* data)
{
if (!data) [[unlikely]]
return false;
@ -304,7 +314,7 @@ bool DirectFilesystemAssetLibrary::LoadMips(const std::filesystem::path& asset_p
if (!File::Exists(full_path))
return true;
VideoCommon::CustomTextureData::Level level;
VideoCommon::CustomTextureData::ArraySlice::Level level;
if (extension_lower == ".dds")
{
if (!LoadDDSTexture(&level, full_path, mip_level))

View File

@ -9,11 +9,10 @@
#include <string>
#include "VideoCommon/Assets/CustomAssetLibrary.h"
#include "VideoCommon/Assets/CustomTextureData.h"
namespace VideoCommon
{
class CustomTextureData;
// This class implements 'CustomAssetLibrary' and loads any assets
// directly from the filesystem
class DirectFilesystemAssetLibrary final : public CustomAssetLibrary
@ -35,7 +34,7 @@ public:
private:
// Loads additional mip levels into the texture structure until _mip<N> texture is not found
bool LoadMips(const std::filesystem::path& asset_path, CustomTextureData* data);
bool LoadMips(const std::filesystem::path& asset_path, CustomTextureData::ArraySlice* data);
// Gets the asset map given an asset id
AssetMap GetAssetMapForID(const AssetID& asset_id) const;

View File

@ -47,7 +47,7 @@ bool GameTextureAsset::Validate(u32 native_width, u32 native_height) const
return false;
}
if (m_data->m_levels.empty())
if (m_data->m_slices.empty())
{
ERROR_LOG_FMT(VIDEO,
"Game texture can't be validated for asset '{}' because no data was available.",
@ -55,9 +55,28 @@ bool GameTextureAsset::Validate(u32 native_width, u32 native_height) const
return false;
}
if (m_data->m_slices.size() > 1)
{
ERROR_LOG_FMT(
VIDEO,
"Game texture can't be validated for asset '{}' because it has more slices than expected.",
GetAssetId());
return false;
}
const auto& slice = m_data->m_slices[0];
if (slice.m_levels.empty())
{
ERROR_LOG_FMT(
VIDEO,
"Game texture can't be validated for asset '{}' because first slice has no data available.",
GetAssetId());
return false;
}
// Verify that the aspect ratio of the texture hasn't changed, as this could have
// side-effects.
const VideoCommon::CustomTextureData::Level& first_mip = m_data->m_levels[0];
const VideoCommon::CustomTextureData::ArraySlice::Level& first_mip = slice.m_levels[0];
if (first_mip.width * native_height != first_mip.height * native_width)
{
// Note: this feels like this should return an error but

View File

@ -423,15 +423,23 @@ void CustomPipelineAction::OnTextureCreate(GraphicsModActionData::TextureCreate*
auto data = game_texture.m_asset->GetData();
if (data)
{
if (create->texture_width != data->m_levels[0].width ||
create->texture_height != data->m_levels[0].height)
if (data->m_slices.empty() || data->m_slices[0].m_levels.empty())
{
ERROR_LOG_FMT(
VIDEO,
"Custom pipeline for texture '{}' has asset '{}' that does not have any texture data",
create->texture_name, game_texture.m_asset->GetAssetId());
m_valid = false;
}
else if (create->texture_width != data->m_slices[0].m_levels[0].width ||
create->texture_height != data->m_slices[0].m_levels[0].height)
{
ERROR_LOG_FMT(VIDEO,
"Custom pipeline for texture '{}' has asset '{}' that does not match "
"the width/height of the texture loaded. Texture {}x{} vs asset {}x{}",
create->texture_name, game_texture.m_asset->GetAssetId(),
create->texture_width, create->texture_height, data->m_levels[0].width,
data->m_levels[0].height);
create->texture_width, create->texture_height,
data->m_slices[0].m_levels[0].width, data->m_slices[0].m_levels[0].height);
m_valid = false;
}
}

View File

@ -1640,10 +1640,13 @@ RcTcacheEntry TextureCacheBase::GetTexture(const int textureCacheSafetyColorSamp
auto data = asset->GetData();
if (data)
{
if (!data->m_levels.empty())
if (!data->m_slices.empty())
{
height = data->m_levels[0].height;
width = data->m_levels[0].width;
if (!data->m_slices[0].m_levels.empty())
{
height = data->m_slices[0].m_levels[0].height;
width = data->m_slices[0].m_levels[0].width;
}
}
}
}
@ -1679,6 +1682,9 @@ RcTcacheEntry TextureCacheBase::GetTexture(const int textureCacheSafetyColorSamp
return entry;
}
// Note: the following function assumes all CustomTextureData has a single slice. This is verified
// with the 'GameTexture::Validate' function after the data is loaded. Only a single slice is
// expected because each texture is loaded into a texture array
RcTcacheEntry TextureCacheBase::CreateTextureEntry(
const TextureCreationInfo& creation_info, const TextureInfo& texture_info,
const int safety_color_sample_size,
@ -1697,12 +1703,12 @@ RcTcacheEntry TextureCacheBase::CreateTextureEntry(
const auto calculate_max_levels = [&]() {
const auto max_element = std::max_element(
assets_data.begin(), assets_data.end(), [](const auto& lhs, const auto& rhs) {
return lhs->m_levels.size() < rhs->m_levels.size();
return lhs->m_slices[0].m_levels.size() < rhs->m_slices[0].m_levels.size();
});
return max_element->get()->m_levels.size();
return max_element->get()->m_slices[0].m_levels.size();
};
const u32 texLevels = no_mips ? 1 : (u32)calculate_max_levels();
const auto& first_level = assets_data[0]->m_levels[0];
const auto& first_level = assets_data[0]->m_slices[0].m_levels[0];
const TextureConfig config(first_level.width, first_level.height, texLevels,
static_cast<u32>(assets_data.size()), 1, first_level.format, 0);
entry = AllocateCacheEntry(config);
@ -1711,11 +1717,12 @@ RcTcacheEntry TextureCacheBase::CreateTextureEntry(
for (u32 data_index = 0; data_index < static_cast<u32>(assets_data.size()); data_index++)
{
const auto asset = assets_data[data_index];
const auto& slice = asset->m_slices[0];
for (u32 level_index = 0;
level_index < std::min(texLevels, static_cast<u32>(asset->m_levels.size()));
level_index < std::min(texLevels, static_cast<u32>(slice.m_levels.size()));
++level_index)
{
const auto& level = asset->m_levels[level_index];
const auto& level = slice.m_levels[level_index];
entry->texture->Load(level_index, level.width, level.height, level.row_length,
level.data.data(), level.data.size(), data_index);
}