VideoCommon: add loading cube maps from DDS files and loading it into our custom texture object. Custom texture object now has the concept of slices in addition to levels. Traditional custom textures have a single slice

This commit is contained in:
iwubcode 2023-08-13 16:09:45 -05:00
parent 5e5887a378
commit 62fee2f3b6
8 changed files with 225 additions and 118 deletions

View File

@ -15,10 +15,13 @@ 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)
{
for (const auto& level : slice.m_levels)
{
total += level.data.size();
}
}
return total;
}
} // namespace
@ -30,52 +33,59 @@ CustomAssetLibrary::LoadInfo CustomAssetLibrary::LoadGameTexture(const AssetID&
return {};
// Note: 'LoadTexture()' ensures we have a level loaded
const auto& first_mip = data->m_levels[0];
for (std::size_t slice_index = 0; slice_index < data->m_slices.size(); slice_index++)
{
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>(data->m_levels.size()); mip_level++)
for (u32 mip_level = 1; mip_level < static_cast<u32>(slice.m_levels.size()); mip_level++)
{
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];
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 {} "
"Invalid custom game texture size {}x{} for texture asset {}. Slice {} with "
"mipmap level {} "
"must be {}x{}.",
level.width, level.height, asset_id, mip_level, current_mip_width,
current_mip_height);
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. Skipping extra levels.",
asset_id);
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();
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) {
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.",
asset_id);
ERROR_LOG_FMT(
VIDEO, "Custom game texture {} has inconsistent formats across mip levels for slice {}.",
asset_id, slice_index);
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,16 +505,21 @@ bool LoadDDSTexture(CustomTextureData* texture, const std::string& filename)
if (!ParseDDSHeader(file, &info))
return false;
if (!file.Seek(info.first_mip_offset, File::SeekOrigin::Begin))
return false;
for (u32 arr_i = 0; arr_i < info.array_size; arr_i++)
{
auto& slice = texture->m_slices.emplace_back();
// 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,
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;
}
texture->m_levels.push_back(std::move(first_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.
@ -488,18 +535,20 @@ bool LoadDDSTexture(CustomTextureData* texture, const std::string& filename)
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;
CustomTextureData::ArraySlice::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(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,6 +14,8 @@ namespace VideoCommon
class CustomTextureData
{
public:
struct ArraySlice
{
struct Level
{
std::vector<u8> data;
@ -24,8 +26,11 @@ public:
};
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,10 +33,13 @@ 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)
{
for (const auto& level : slice.m_levels)
{
total += level.data.size();
}
}
return total;
}
} // namespace
@ -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

@ -1639,10 +1639,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;
}
}
}
}
@ -1678,6 +1681,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,
@ -1696,12 +1702,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);
@ -1710,11 +1716,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);
}