1101 lines
32 KiB
C++
1101 lines
32 KiB
C++
/* Copyright (C) 2022 Wildfire Games.
|
|
* This file is part of 0 A.D.
|
|
*
|
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* 0 A.D. is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "precompiled.h"
|
|
|
|
#include "TextureManager.h"
|
|
|
|
#include "graphics/Color.h"
|
|
#include "graphics/TextureConverter.h"
|
|
#include "lib/allocators/shared_ptr.h"
|
|
#include "lib/bits.h"
|
|
#include "lib/file/vfs/vfs_tree.h"
|
|
#include "lib/hash.h"
|
|
#include "lib/timer.h"
|
|
#include "maths/MathUtil.h"
|
|
#include "maths/MD5.h"
|
|
#include "ps/CacheLoader.h"
|
|
#include "ps/CLogger.h"
|
|
#include "ps/ConfigDB.h"
|
|
#include "ps/Filesystem.h"
|
|
#include "ps/Profile.h"
|
|
#include "ps/Util.h"
|
|
#include "renderer/backend/IDevice.h"
|
|
#include "renderer/Renderer.h"
|
|
|
|
#include <algorithm>
|
|
#include <boost/filesystem.hpp>
|
|
#include <iomanip>
|
|
#include <set>
|
|
#include <sstream>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
|
|
namespace
|
|
{
|
|
|
|
Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded(Tex& textureData, const bool hasS3TC)
|
|
{
|
|
const bool alpha = (textureData.m_Flags & TEX_ALPHA) != 0;
|
|
const bool grey = (textureData.m_Flags & TEX_GREY) != 0;
|
|
const size_t dxt = textureData.m_Flags & TEX_DXT;
|
|
|
|
// Some backends don't support BGR as an internal format (like GLES).
|
|
// TODO: add a check that the format is internally supported.
|
|
if ((textureData.m_Flags & TEX_BGR) != 0)
|
|
{
|
|
LOGWARNING("Using slow path to convert BGR texture.");
|
|
textureData.transform_to(textureData.m_Flags & ~TEX_BGR);
|
|
}
|
|
|
|
if (dxt)
|
|
{
|
|
if (hasS3TC)
|
|
{
|
|
switch (dxt)
|
|
{
|
|
case DXT1A:
|
|
return Renderer::Backend::Format::BC1_RGBA_UNORM;
|
|
case 1:
|
|
return Renderer::Backend::Format::BC1_RGB_UNORM;
|
|
case 3:
|
|
return Renderer::Backend::Format::BC2_UNORM;
|
|
case 5:
|
|
return Renderer::Backend::Format::BC3_UNORM;
|
|
default:
|
|
LOGERROR("Unknown DXT compression.");
|
|
return Renderer::Backend::Format::UNDEFINED;
|
|
}
|
|
}
|
|
else
|
|
textureData.transform_to(textureData.m_Flags & ~TEX_DXT);
|
|
}
|
|
|
|
switch (textureData.m_Bpp)
|
|
{
|
|
case 8:
|
|
ENSURE(grey);
|
|
return Renderer::Backend::Format::L8_UNORM;
|
|
case 24:
|
|
ENSURE(!alpha);
|
|
return Renderer::Backend::Format::R8G8B8_UNORM;
|
|
case 32:
|
|
ENSURE(alpha);
|
|
return Renderer::Backend::Format::R8G8B8A8_UNORM;
|
|
default:
|
|
LOGERROR("Unsupported BPP: %zu", textureData.m_Bpp);
|
|
}
|
|
|
|
return Renderer::Backend::Format::UNDEFINED;
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
class CPredefinedTexture
|
|
{
|
|
public:
|
|
const CTexturePtr& GetTexture()
|
|
{
|
|
return m_Texture;
|
|
}
|
|
|
|
void CreateTexture(
|
|
std::unique_ptr<Renderer::Backend::ITexture> backendTexture,
|
|
CTextureManagerImpl* textureManager)
|
|
{
|
|
Renderer::Backend::ITexture* fallback = backendTexture.get();
|
|
CTextureProperties props(VfsPath{});
|
|
m_Texture = CTexturePtr(new CTexture(
|
|
std::move(backendTexture), fallback, props, textureManager));
|
|
m_Texture->m_State = CTexture::UPLOADED;
|
|
m_Texture->m_Self = m_Texture;
|
|
}
|
|
|
|
private:
|
|
CTexturePtr m_Texture;
|
|
};
|
|
|
|
class CSingleColorTexture final : public CPredefinedTexture
|
|
{
|
|
public:
|
|
CSingleColorTexture(const CColor& color, Renderer::Backend::IDevice* device,
|
|
CTextureManagerImpl* textureManager)
|
|
: m_Color(color)
|
|
{
|
|
std::stringstream textureName;
|
|
textureName << "SingleColorTexture (";
|
|
textureName << "R:" << m_Color.r << ", ";
|
|
textureName << "G:" << m_Color.g << ", ";
|
|
textureName << "B:" << m_Color.b << ", ";
|
|
textureName << "A:" << m_Color.a << ")";
|
|
|
|
std::unique_ptr<Renderer::Backend::ITexture> backendTexture =
|
|
device->CreateTexture2D(
|
|
textureName.str().c_str(),
|
|
Renderer::Backend::Format::R8G8B8A8_UNORM,
|
|
1, 1, Renderer::Backend::Sampler::MakeDefaultSampler(
|
|
Renderer::Backend::Sampler::Filter::LINEAR,
|
|
Renderer::Backend::Sampler::AddressMode::REPEAT));
|
|
CreateTexture(std::move(backendTexture), textureManager);
|
|
}
|
|
|
|
void Upload(Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
|
|
{
|
|
if (!GetTexture() || !GetTexture()->GetBackendTexture())
|
|
return;
|
|
|
|
const SColor4ub color32 = m_Color.AsSColor4ub();
|
|
// Construct 1x1 32-bit texture
|
|
const u8 data[4] =
|
|
{
|
|
color32.R,
|
|
color32.G,
|
|
color32.B,
|
|
color32.A
|
|
};
|
|
deviceCommandContext->UploadTexture(GetTexture()->GetBackendTexture(),
|
|
Renderer::Backend::Format::R8G8B8A8_UNORM, data, std::size(data));
|
|
}
|
|
|
|
private:
|
|
CColor m_Color;
|
|
};
|
|
|
|
class CSingleColorTextureCube final : public CPredefinedTexture
|
|
{
|
|
public:
|
|
CSingleColorTextureCube(const CColor& color, Renderer::Backend::IDevice* device,
|
|
CTextureManagerImpl* textureManager)
|
|
: m_Color(color)
|
|
{
|
|
std::stringstream textureName;
|
|
textureName << "SingleColorTextureCube (";
|
|
textureName << "R:" << m_Color.r << ", ";
|
|
textureName << "G:" << m_Color.g << ", ";
|
|
textureName << "B:" << m_Color.b << ", ";
|
|
textureName << "A:" << m_Color.a << ")";
|
|
|
|
std::unique_ptr<Renderer::Backend::ITexture> backendTexture =
|
|
device->CreateTexture(
|
|
textureName.str().c_str(), Renderer::Backend::ITexture::Type::TEXTURE_CUBE,
|
|
Renderer::Backend::Format::R8G8B8A8_UNORM,
|
|
1, 1, Renderer::Backend::Sampler::MakeDefaultSampler(
|
|
Renderer::Backend::Sampler::Filter::LINEAR,
|
|
Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE), 1, 1);
|
|
CreateTexture(std::move(backendTexture), textureManager);
|
|
}
|
|
|
|
void Upload(Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
|
|
{
|
|
if (!GetTexture() || !GetTexture()->GetBackendTexture())
|
|
return;
|
|
|
|
const SColor4ub color32 = m_Color.AsSColor4ub();
|
|
// Construct 1x1 32-bit texture
|
|
const u8 data[4] =
|
|
{
|
|
color32.R,
|
|
color32.G,
|
|
color32.B,
|
|
color32.A
|
|
};
|
|
|
|
for (size_t face = 0; face < 6; ++face)
|
|
{
|
|
deviceCommandContext->UploadTexture(
|
|
GetTexture()->GetBackendTexture(), Renderer::Backend::Format::R8G8B8A8_UNORM,
|
|
data, std::size(data), 0, face);
|
|
}
|
|
}
|
|
|
|
private:
|
|
CColor m_Color;
|
|
};
|
|
|
|
class CGradientTexture final : public CPredefinedTexture
|
|
{
|
|
public:
|
|
static const uint32_t WIDTH = 256;
|
|
static const uint32_t NUMBER_OF_LEVELS = 9;
|
|
|
|
CGradientTexture(const CColor& colorFrom, const CColor& colorTo,
|
|
Renderer::Backend::IDevice* device, CTextureManagerImpl* textureManager)
|
|
: m_ColorFrom(colorFrom), m_ColorTo(colorTo)
|
|
{
|
|
std::stringstream textureName;
|
|
textureName << "GradientTexture";
|
|
textureName << " From (";
|
|
textureName << "R:" << m_ColorFrom.r << ", ";
|
|
textureName << "G:" << m_ColorFrom.g << ", ";
|
|
textureName << "B:" << m_ColorFrom.b << ", ";
|
|
textureName << "A:" << m_ColorFrom.a << ")";
|
|
textureName << " To (";
|
|
textureName << "R:" << m_ColorTo.r << ", ";
|
|
textureName << "G:" << m_ColorTo.g << ", ";
|
|
textureName << "B:" << m_ColorTo.b << ", ";
|
|
textureName << "A:" << m_ColorTo.a << ")";
|
|
|
|
std::unique_ptr<Renderer::Backend::ITexture> backendTexture =
|
|
device->CreateTexture2D(
|
|
textureName.str().c_str(),
|
|
Renderer::Backend::Format::R8G8B8A8_UNORM,
|
|
WIDTH, 1, Renderer::Backend::Sampler::MakeDefaultSampler(
|
|
Renderer::Backend::Sampler::Filter::LINEAR,
|
|
Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE),
|
|
NUMBER_OF_LEVELS);
|
|
CreateTexture(std::move(backendTexture), textureManager);
|
|
}
|
|
|
|
void Upload(Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
|
|
{
|
|
if (!GetTexture() || !GetTexture()->GetBackendTexture())
|
|
return;
|
|
|
|
std::array<std::array<u8, 4>, WIDTH> data;
|
|
for (uint32_t x = 0; x < WIDTH; ++x)
|
|
{
|
|
const float t = static_cast<float>(x) / (WIDTH - 1);
|
|
const CColor color(
|
|
Interpolate(m_ColorFrom.r, m_ColorTo.r, t),
|
|
Interpolate(m_ColorFrom.g, m_ColorTo.g, t),
|
|
Interpolate(m_ColorFrom.b, m_ColorTo.b, t),
|
|
Interpolate(m_ColorFrom.a, m_ColorTo.a, t));
|
|
const SColor4ub color32 = color.AsSColor4ub();
|
|
data[x][0] = color32.R;
|
|
data[x][1] = color32.G;
|
|
data[x][2] = color32.B;
|
|
data[x][3] = color32.A;
|
|
}
|
|
for (uint32_t level = 0; level < NUMBER_OF_LEVELS; ++level)
|
|
{
|
|
deviceCommandContext->UploadTexture(GetTexture()->GetBackendTexture(),
|
|
Renderer::Backend::Format::R8G8B8A8_UNORM, data.data(), (WIDTH >> level) * data[0].size(), level);
|
|
// Prepare data for the next level.
|
|
const uint32_t nextLevelWidth = (WIDTH >> (level + 1));
|
|
if (nextLevelWidth > 0)
|
|
{
|
|
for (uint32_t x = 0; x < nextLevelWidth; ++x)
|
|
data[x] = data[(x << 1)];
|
|
// Border values should be the same.
|
|
data[nextLevelWidth - 1] = data[(WIDTH >> level) - 1];
|
|
}
|
|
}
|
|
}
|
|
|
|
private:
|
|
CColor m_ColorFrom, m_ColorTo;
|
|
};
|
|
|
|
struct TPhash
|
|
{
|
|
std::size_t operator()(const CTextureProperties& textureProperties) const
|
|
{
|
|
std::size_t seed = 0;
|
|
hash_combine(seed, m_PathHash(textureProperties.m_Path));
|
|
hash_combine(seed, textureProperties.m_AddressModeU);
|
|
hash_combine(seed, textureProperties.m_AddressModeV);
|
|
hash_combine(seed, textureProperties.m_AnisotropicFilterEnabled);
|
|
hash_combine(seed, textureProperties.m_FormatOverride);
|
|
hash_combine(seed, textureProperties.m_IgnoreQuality);
|
|
return seed;
|
|
}
|
|
|
|
std::size_t operator()(const CTexturePtr& texture) const
|
|
{
|
|
return this->operator()(texture->m_Properties);
|
|
}
|
|
|
|
private:
|
|
std::hash<Path> m_PathHash;
|
|
};
|
|
|
|
struct TPequal_to
|
|
{
|
|
bool operator()(const CTextureProperties& lhs, const CTextureProperties& rhs) const
|
|
{
|
|
return
|
|
lhs.m_Path == rhs.m_Path &&
|
|
lhs.m_AddressModeU == rhs.m_AddressModeU &&
|
|
lhs.m_AddressModeV == rhs.m_AddressModeV &&
|
|
lhs.m_AnisotropicFilterEnabled == rhs.m_AnisotropicFilterEnabled &&
|
|
lhs.m_FormatOverride == rhs.m_FormatOverride &&
|
|
lhs.m_IgnoreQuality == rhs.m_IgnoreQuality;
|
|
}
|
|
|
|
bool operator()(const CTexturePtr& lhs, const CTexturePtr& rhs) const
|
|
{
|
|
return this->operator()(lhs->m_Properties, rhs->m_Properties);
|
|
}
|
|
};
|
|
|
|
class CTextureManagerImpl
|
|
{
|
|
friend class CTexture;
|
|
public:
|
|
CTextureManagerImpl(PIVFS vfs, bool highQuality, Renderer::Backend::IDevice* device) :
|
|
m_VFS(vfs), m_CacheLoader(vfs, L".dds"), m_Device(device),
|
|
m_TextureConverter(vfs, highQuality),
|
|
m_DefaultTexture(CColor(0.25f, 0.25f, 0.25f, 1.0f), device, this),
|
|
m_ErrorTexture(CColor(1.0f, 0.0f, 1.0f, 1.0f), device, this),
|
|
m_WhiteTexture(CColor(1.0f, 1.0f, 1.0f, 1.0f), device, this),
|
|
m_TransparentTexture(CColor(0.0f, 0.0f, 0.0f, 0.0f), device, this),
|
|
m_AlphaGradientTexture(
|
|
CColor(1.0f, 1.0f, 1.0f, 0.0f), CColor(1.0f, 1.0f, 1.0f, 1.0f), device, this),
|
|
m_BlackTextureCube(CColor(0.0f, 0.0f, 0.0f, 1.0f), device, this)
|
|
{
|
|
// Allow hotloading of textures
|
|
RegisterFileReloadFunc(ReloadChangedFileCB, this);
|
|
|
|
m_HasS3TC =
|
|
m_Device->IsTextureFormatSupported(Renderer::Backend::Format::BC1_RGB_UNORM) &&
|
|
m_Device->IsTextureFormatSupported(Renderer::Backend::Format::BC1_RGBA_UNORM) &&
|
|
m_Device->IsTextureFormatSupported(Renderer::Backend::Format::BC2_UNORM) &&
|
|
m_Device->IsTextureFormatSupported(Renderer::Backend::Format::BC3_UNORM);
|
|
}
|
|
|
|
~CTextureManagerImpl()
|
|
{
|
|
UnregisterFileReloadFunc(ReloadChangedFileCB, this);
|
|
}
|
|
|
|
const CTexturePtr& GetErrorTexture()
|
|
{
|
|
return m_ErrorTexture.GetTexture();
|
|
}
|
|
|
|
const CTexturePtr& GetWhiteTexture()
|
|
{
|
|
return m_WhiteTexture.GetTexture();
|
|
}
|
|
|
|
const CTexturePtr& GetTransparentTexture()
|
|
{
|
|
return m_TransparentTexture.GetTexture();
|
|
}
|
|
|
|
const CTexturePtr& GetAlphaGradientTexture()
|
|
{
|
|
return m_AlphaGradientTexture.GetTexture();
|
|
}
|
|
|
|
const CTexturePtr& GetBlackTextureCube()
|
|
{
|
|
return m_BlackTextureCube.GetTexture();
|
|
}
|
|
|
|
/**
|
|
* See CTextureManager::CreateTexture
|
|
*/
|
|
CTexturePtr CreateTexture(const CTextureProperties& props)
|
|
{
|
|
// Construct a new default texture with the given properties to use as the search key
|
|
CTexturePtr texture(new CTexture(
|
|
nullptr, m_DefaultTexture.GetTexture()->GetBackendTexture(), props, this));
|
|
|
|
// Try to find an existing texture with the given properties
|
|
TextureCache::iterator it = m_TextureCache.find(texture);
|
|
if (it != m_TextureCache.end())
|
|
return *it;
|
|
|
|
// Can't find an existing texture - finish setting up this new texture
|
|
texture->m_Self = texture;
|
|
m_TextureCache.insert(texture);
|
|
m_HotloadFiles[props.m_Path].insert(texture);
|
|
|
|
return texture;
|
|
}
|
|
|
|
CTexturePtr WrapBackendTexture(
|
|
std::unique_ptr<Renderer::Backend::ITexture> backendTexture)
|
|
{
|
|
ENSURE(backendTexture);
|
|
Renderer::Backend::ITexture* fallback = backendTexture.get();
|
|
|
|
CTextureProperties props(VfsPath{});
|
|
CTexturePtr texture(new CTexture(
|
|
std::move(backendTexture), fallback, props, this));
|
|
texture->m_State = CTexture::UPLOADED;
|
|
texture->m_Self = texture;
|
|
return texture;
|
|
}
|
|
|
|
/**
|
|
* Load the given file into the texture object and upload it to OpenGL.
|
|
* Assumes the file already exists.
|
|
*/
|
|
void LoadTexture(const CTexturePtr& texture, const VfsPath& path)
|
|
{
|
|
PROFILE2("load texture");
|
|
PROFILE2_ATTR("name: %ls", path.string().c_str());
|
|
|
|
std::shared_ptr<u8> fileData;
|
|
size_t fileSize;
|
|
const Status loadStatus = m_VFS->LoadFile(path, fileData, fileSize);
|
|
if (loadStatus != INFO::OK)
|
|
{
|
|
LOGERROR("Texture failed to load; \"%s\" %s",
|
|
texture->m_Properties.m_Path.string8(), GetStatusAsString(loadStatus).c_str());
|
|
texture->ResetBackendTexture(
|
|
nullptr, m_ErrorTexture.GetTexture()->GetBackendTexture());
|
|
texture->m_TextureData.reset();
|
|
return;
|
|
}
|
|
|
|
texture->m_TextureData = std::make_unique<Tex>();
|
|
Tex& textureData = *texture->m_TextureData;
|
|
const Status decodeStatus = textureData.decode(fileData, fileSize);
|
|
if (decodeStatus != INFO::OK)
|
|
{
|
|
LOGERROR("Texture failed to decode; \"%s\" %s",
|
|
texture->m_Properties.m_Path.string8(), GetStatusAsString(decodeStatus).c_str());
|
|
texture->ResetBackendTexture(
|
|
nullptr, m_ErrorTexture.GetTexture()->GetBackendTexture());
|
|
texture->m_TextureData.reset();
|
|
return;
|
|
}
|
|
|
|
if (!is_pow2(textureData.m_Width) || !is_pow2(textureData.m_Height))
|
|
{
|
|
LOGERROR("Texture should have width and height be power of two; \"%s\" %zux%zu",
|
|
texture->m_Properties.m_Path.string8(), textureData.m_Width, textureData.m_Height);
|
|
texture->ResetBackendTexture(
|
|
nullptr, m_ErrorTexture.GetTexture()->GetBackendTexture());
|
|
texture->m_TextureData.reset();
|
|
return;
|
|
}
|
|
|
|
// Initialise base color from the texture
|
|
texture->m_BaseColor = textureData.get_average_color();
|
|
|
|
Renderer::Backend::Format format = Renderer::Backend::Format::UNDEFINED;
|
|
if (texture->m_Properties.m_FormatOverride != Renderer::Backend::Format::UNDEFINED)
|
|
{
|
|
format = texture->m_Properties.m_FormatOverride;
|
|
// TODO: it'd be good to remove the override hack and provide information
|
|
// via XML.
|
|
ENSURE((textureData.m_Flags & TEX_DXT) == 0);
|
|
if (format == Renderer::Backend::Format::A8_UNORM)
|
|
{
|
|
ENSURE(textureData.m_Bpp == 8 && (textureData.m_Flags & TEX_GREY));
|
|
}
|
|
else if (format == Renderer::Backend::Format::R8G8B8A8_UNORM)
|
|
{
|
|
ENSURE(textureData.m_Bpp == 32 && (textureData.m_Flags & TEX_ALPHA));
|
|
}
|
|
else
|
|
debug_warn("Unsupported format override.");
|
|
}
|
|
else
|
|
{
|
|
format = ChooseFormatAndTransformTextureDataIfNeeded(textureData, m_HasS3TC);
|
|
}
|
|
|
|
if (format == Renderer::Backend::Format::UNDEFINED)
|
|
{
|
|
LOGERROR("Texture failed to choose format; \"%s\"", texture->m_Properties.m_Path.string8());
|
|
texture->ResetBackendTexture(
|
|
nullptr, m_ErrorTexture.GetTexture()->GetBackendTexture());
|
|
texture->m_TextureData.reset();
|
|
return;
|
|
}
|
|
|
|
const uint32_t width = texture->m_TextureData->m_Width;
|
|
const uint32_t height = texture->m_TextureData->m_Height ;
|
|
const uint32_t MIPLevelCount = texture->m_TextureData->GetMIPLevels().size();
|
|
texture->m_BaseLevelOffset = 0;
|
|
|
|
Renderer::Backend::Sampler::Desc defaultSamplerDesc =
|
|
Renderer::Backend::Sampler::MakeDefaultSampler(
|
|
Renderer::Backend::Sampler::Filter::LINEAR,
|
|
Renderer::Backend::Sampler::AddressMode::REPEAT);
|
|
|
|
defaultSamplerDesc.addressModeU = texture->m_Properties.m_AddressModeU;
|
|
defaultSamplerDesc.addressModeV = texture->m_Properties.m_AddressModeV;
|
|
if (texture->m_Properties.m_AnisotropicFilterEnabled && m_Device->GetCapabilities().anisotropicFiltering)
|
|
{
|
|
int maxAnisotropy = 1;
|
|
CFG_GET_VAL("textures.maxanisotropy", maxAnisotropy);
|
|
const int allowedValues[] = {2, 4, 8, 16};
|
|
if (std::find(std::begin(allowedValues), std::end(allowedValues), maxAnisotropy) != std::end(allowedValues))
|
|
{
|
|
defaultSamplerDesc.anisotropyEnabled = true;
|
|
defaultSamplerDesc.maxAnisotropy = maxAnisotropy;
|
|
}
|
|
}
|
|
|
|
if (!texture->m_Properties.m_IgnoreQuality)
|
|
{
|
|
int quality = 2;
|
|
CFG_GET_VAL("textures.quality", quality);
|
|
if (quality == 1)
|
|
{
|
|
if (MIPLevelCount > 1 && std::min(width, height) > 8)
|
|
texture->m_BaseLevelOffset += 1;
|
|
}
|
|
else if (quality == 0)
|
|
{
|
|
if (MIPLevelCount > 2 && std::min(width, height) > 16)
|
|
texture->m_BaseLevelOffset += 2;
|
|
while (std::min(width >> texture->m_BaseLevelOffset, height >> texture->m_BaseLevelOffset) > 256 &&
|
|
MIPLevelCount > texture->m_BaseLevelOffset + 1)
|
|
{
|
|
texture->m_BaseLevelOffset += 1;
|
|
}
|
|
defaultSamplerDesc.mipFilter = Renderer::Backend::Sampler::Filter::NEAREST;
|
|
defaultSamplerDesc.anisotropyEnabled = false;
|
|
}
|
|
}
|
|
|
|
texture->m_BackendTexture = m_Device->CreateTexture2D(
|
|
texture->m_Properties.m_Path.string8().c_str(),
|
|
format, (width >> texture->m_BaseLevelOffset), (height >> texture->m_BaseLevelOffset),
|
|
defaultSamplerDesc, MIPLevelCount - texture->m_BaseLevelOffset);
|
|
}
|
|
|
|
/**
|
|
* Set up some parameters for the loose cache filename code.
|
|
*/
|
|
void PrepareCacheKey(const CTexturePtr& texture, MD5& hash, u32& version)
|
|
{
|
|
// Hash the settings, so we won't use an old loose cache file if the
|
|
// settings have changed
|
|
CTextureConverter::Settings settings = GetConverterSettings(texture);
|
|
settings.Hash(hash);
|
|
|
|
// Arbitrary version number - change this if we update the code and
|
|
// need to invalidate old users' caches
|
|
version = 1;
|
|
}
|
|
|
|
/**
|
|
* Attempts to load a cached version of a texture.
|
|
* If the texture is loaded (or there was an error), returns true.
|
|
* Otherwise, returns false to indicate the caller should generate the cached version.
|
|
*/
|
|
bool TryLoadingCached(const CTexturePtr& texture)
|
|
{
|
|
MD5 hash;
|
|
u32 version;
|
|
PrepareCacheKey(texture, hash, version);
|
|
|
|
VfsPath loadPath;
|
|
Status ret = m_CacheLoader.TryLoadingCached(texture->m_Properties.m_Path, hash, version, loadPath);
|
|
|
|
if (ret == INFO::OK)
|
|
{
|
|
// Found a cached texture - load it
|
|
LoadTexture(texture, loadPath);
|
|
return true;
|
|
}
|
|
else if (ret == INFO::SKIPPED)
|
|
{
|
|
// No cached version was found - we'll need to create it
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
ENSURE(ret < 0);
|
|
|
|
// No source file or archive cache was found, so we can't load the
|
|
// real texture at all - return the error texture instead
|
|
LOGERROR("CCacheLoader failed to find archived or source file for: \"%s\"", texture->m_Properties.m_Path.string8());
|
|
texture->ResetBackendTexture(
|
|
nullptr, m_ErrorTexture.GetTexture()->GetBackendTexture());
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initiates an asynchronous conversion process, from the texture's
|
|
* source file to the corresponding loose cache file.
|
|
*/
|
|
void ConvertTexture(const CTexturePtr& texture)
|
|
{
|
|
const VfsPath sourcePath = texture->m_Properties.m_Path;
|
|
|
|
PROFILE2("convert texture");
|
|
PROFILE2_ATTR("name: %ls", sourcePath.string().c_str());
|
|
|
|
MD5 hash;
|
|
u32 version;
|
|
PrepareCacheKey(texture, hash, version);
|
|
const VfsPath looseCachePath = m_CacheLoader.LooseCachePath(sourcePath, hash, version);
|
|
|
|
CTextureConverter::Settings settings = GetConverterSettings(texture);
|
|
|
|
m_TextureConverter.ConvertTexture(texture, sourcePath, looseCachePath, settings);
|
|
}
|
|
|
|
bool TextureExists(const VfsPath& path) const
|
|
{
|
|
return m_VFS->GetFileInfo(m_CacheLoader.ArchiveCachePath(path), 0) == INFO::OK ||
|
|
m_VFS->GetFileInfo(path, 0) == INFO::OK;
|
|
}
|
|
|
|
bool GenerateCachedTexture(const VfsPath& sourcePath, VfsPath& archiveCachePath)
|
|
{
|
|
archiveCachePath = m_CacheLoader.ArchiveCachePath(sourcePath);
|
|
|
|
CTextureProperties textureProps(sourcePath);
|
|
CTexturePtr texture = CreateTexture(textureProps);
|
|
CTextureConverter::Settings settings = GetConverterSettings(texture);
|
|
|
|
if (!m_TextureConverter.ConvertTexture(texture, sourcePath, VfsPath("cache") / archiveCachePath, settings))
|
|
return false;
|
|
|
|
while (true)
|
|
{
|
|
CTexturePtr textureOut;
|
|
VfsPath dest;
|
|
bool ok;
|
|
if (m_TextureConverter.Poll(textureOut, dest, ok))
|
|
return ok;
|
|
|
|
std::this_thread::sleep_for(std::chrono::microseconds(1));
|
|
}
|
|
}
|
|
|
|
bool MakeProgress()
|
|
{
|
|
// Process any completed conversion tasks
|
|
{
|
|
CTexturePtr texture;
|
|
VfsPath dest;
|
|
bool ok;
|
|
if (m_TextureConverter.Poll(texture, dest, ok))
|
|
{
|
|
if (ok)
|
|
{
|
|
LoadTexture(texture, dest);
|
|
}
|
|
else
|
|
{
|
|
LOGERROR("Texture failed to convert: \"%s\"", texture->m_Properties.m_Path.string8());
|
|
texture->ResetBackendTexture(
|
|
nullptr, m_ErrorTexture.GetTexture()->GetBackendTexture());
|
|
}
|
|
texture->m_State = CTexture::LOADED;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// We'll only push new conversion requests if it's not already busy
|
|
bool converterBusy = m_TextureConverter.IsBusy();
|
|
|
|
if (!converterBusy)
|
|
{
|
|
// Look for all high-priority textures needing conversion.
|
|
// (Iterating over all textures isn't optimally efficient, but it
|
|
// doesn't seem to be a problem yet and it's simpler than maintaining
|
|
// multiple queues.)
|
|
for (TextureCache::iterator it = m_TextureCache.begin(); it != m_TextureCache.end(); ++it)
|
|
{
|
|
if ((*it)->m_State == CTexture::HIGH_NEEDS_CONVERTING)
|
|
{
|
|
// Start converting this texture
|
|
(*it)->m_State = CTexture::HIGH_IS_CONVERTING;
|
|
ConvertTexture(*it);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try loading prefetched textures from their cache
|
|
for (TextureCache::iterator it = m_TextureCache.begin(); it != m_TextureCache.end(); ++it)
|
|
{
|
|
if ((*it)->m_State == CTexture::PREFETCH_NEEDS_LOADING)
|
|
{
|
|
if (TryLoadingCached(*it))
|
|
{
|
|
(*it)->m_State = CTexture::LOADED;
|
|
}
|
|
else
|
|
{
|
|
(*it)->m_State = CTexture::PREFETCH_NEEDS_CONVERTING;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// If we've got nothing better to do, then start converting prefetched textures.
|
|
if (!converterBusy)
|
|
{
|
|
for (TextureCache::iterator it = m_TextureCache.begin(); it != m_TextureCache.end(); ++it)
|
|
{
|
|
if ((*it)->m_State == CTexture::PREFETCH_NEEDS_CONVERTING)
|
|
{
|
|
(*it)->m_State = CTexture::PREFETCH_IS_CONVERTING;
|
|
ConvertTexture(*it);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool MakeUploadProgress(
|
|
Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
|
|
{
|
|
if (!m_PredefinedTexturesUploaded)
|
|
{
|
|
m_DefaultTexture.Upload(deviceCommandContext);
|
|
m_ErrorTexture.Upload(deviceCommandContext);
|
|
m_WhiteTexture.Upload(deviceCommandContext);
|
|
m_TransparentTexture.Upload(deviceCommandContext);
|
|
m_AlphaGradientTexture.Upload(deviceCommandContext);
|
|
m_BlackTextureCube.Upload(deviceCommandContext);
|
|
m_PredefinedTexturesUploaded = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Compute the conversion settings that apply to a given texture, by combining
|
|
* the textures.xml files from its directory and all parent directories
|
|
* (up to the VFS root).
|
|
*/
|
|
CTextureConverter::Settings GetConverterSettings(const CTexturePtr& texture)
|
|
{
|
|
fs::wpath srcPath = texture->m_Properties.m_Path.string();
|
|
|
|
std::vector<CTextureConverter::SettingsFile*> files;
|
|
VfsPath p;
|
|
for (fs::wpath::iterator it = srcPath.begin(); it != srcPath.end(); ++it)
|
|
{
|
|
VfsPath settingsPath = p / "textures.xml";
|
|
m_HotloadFiles[settingsPath].insert(texture);
|
|
CTextureConverter::SettingsFile* f = GetSettingsFile(settingsPath);
|
|
if (f)
|
|
files.push_back(f);
|
|
p = p / GetWstringFromWpath(*it);
|
|
}
|
|
return m_TextureConverter.ComputeSettings(GetWstringFromWpath(srcPath.leaf()), files);
|
|
}
|
|
|
|
/**
|
|
* Return the (cached) settings file with the given filename,
|
|
* or NULL if it doesn't exist.
|
|
*/
|
|
CTextureConverter::SettingsFile* GetSettingsFile(const VfsPath& path)
|
|
{
|
|
SettingsFilesMap::iterator it = m_SettingsFiles.find(path);
|
|
if (it != m_SettingsFiles.end())
|
|
return it->second.get();
|
|
|
|
if (m_VFS->GetFileInfo(path, NULL) >= 0)
|
|
{
|
|
std::shared_ptr<CTextureConverter::SettingsFile> settings(m_TextureConverter.LoadSettings(path));
|
|
m_SettingsFiles.insert(std::make_pair(path, settings));
|
|
return settings.get();
|
|
}
|
|
else
|
|
{
|
|
m_SettingsFiles.insert(std::make_pair(path, std::shared_ptr<CTextureConverter::SettingsFile>()));
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
static Status ReloadChangedFileCB(void* param, const VfsPath& path)
|
|
{
|
|
return static_cast<CTextureManagerImpl*>(param)->ReloadChangedFile(path);
|
|
}
|
|
|
|
Status ReloadChangedFile(const VfsPath& path)
|
|
{
|
|
// Uncache settings file, if this is one
|
|
m_SettingsFiles.erase(path);
|
|
|
|
// Find all textures using this file
|
|
HotloadFilesMap::iterator files = m_HotloadFiles.find(path);
|
|
if (files != m_HotloadFiles.end())
|
|
{
|
|
// Flag all textures using this file as needing reloading
|
|
for (std::set<std::weak_ptr<CTexture>>::iterator it = files->second.begin(); it != files->second.end(); ++it)
|
|
{
|
|
if (std::shared_ptr<CTexture> texture = it->lock())
|
|
{
|
|
texture->m_State = CTexture::UNLOADED;
|
|
texture->ResetBackendTexture(
|
|
nullptr, m_DefaultTexture.GetTexture()->GetBackendTexture());
|
|
texture->m_TextureData.reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
return INFO::OK;
|
|
}
|
|
|
|
void ReloadAllTextures()
|
|
{
|
|
for (const CTexturePtr& texture : m_TextureCache)
|
|
{
|
|
texture->m_State = CTexture::UNLOADED;
|
|
texture->ResetBackendTexture(
|
|
nullptr, m_DefaultTexture.GetTexture()->GetBackendTexture());
|
|
texture->m_TextureData.reset();
|
|
}
|
|
}
|
|
|
|
size_t GetBytesUploaded() const
|
|
{
|
|
size_t size = 0;
|
|
for (TextureCache::const_iterator it = m_TextureCache.begin(); it != m_TextureCache.end(); ++it)
|
|
size += (*it)->GetUploadedSize();
|
|
return size;
|
|
}
|
|
|
|
void OnQualityChanged()
|
|
{
|
|
ReloadAllTextures();
|
|
}
|
|
|
|
private:
|
|
PIVFS m_VFS;
|
|
CCacheLoader m_CacheLoader;
|
|
Renderer::Backend::IDevice* m_Device = nullptr;
|
|
CTextureConverter m_TextureConverter;
|
|
|
|
CSingleColorTexture m_DefaultTexture;
|
|
CSingleColorTexture m_ErrorTexture;
|
|
CSingleColorTexture m_WhiteTexture;
|
|
CSingleColorTexture m_TransparentTexture;
|
|
CGradientTexture m_AlphaGradientTexture;
|
|
CSingleColorTextureCube m_BlackTextureCube;
|
|
bool m_PredefinedTexturesUploaded = false;
|
|
|
|
// Cache of all loaded textures
|
|
using TextureCache =
|
|
std::unordered_set<CTexturePtr, TPhash, TPequal_to>;
|
|
TextureCache m_TextureCache;
|
|
// TODO: we ought to expire unused textures from the cache eventually
|
|
|
|
// Store the set of textures that need to be reloaded when the given file
|
|
// (a source file or settings.xml) is modified
|
|
using HotloadFilesMap =
|
|
std::unordered_map<VfsPath, std::set<std::weak_ptr<CTexture>, std::owner_less<std::weak_ptr<CTexture>>>>;
|
|
HotloadFilesMap m_HotloadFiles;
|
|
|
|
// Cache for the conversion settings files
|
|
using SettingsFilesMap =
|
|
std::unordered_map<VfsPath, std::shared_ptr<CTextureConverter::SettingsFile>>;
|
|
SettingsFilesMap m_SettingsFiles;
|
|
|
|
bool m_HasS3TC = false;
|
|
};
|
|
|
|
CTexture::CTexture(
|
|
std::unique_ptr<Renderer::Backend::ITexture> texture,
|
|
Renderer::Backend::ITexture* fallback,
|
|
const CTextureProperties& props, CTextureManagerImpl* textureManager) :
|
|
m_BackendTexture(std::move(texture)), m_FallbackBackendTexture(fallback),
|
|
m_BaseColor(0), m_State(UNLOADED), m_Properties(props),
|
|
m_TextureManager(textureManager)
|
|
{
|
|
}
|
|
|
|
CTexture::~CTexture() = default;
|
|
|
|
void CTexture::UploadBackendTextureIfNeeded(
|
|
Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
|
|
{
|
|
if (IsUploaded())
|
|
return;
|
|
|
|
if (!IsLoaded())
|
|
TryLoad();
|
|
|
|
if (!IsLoaded())
|
|
return;
|
|
else if (!m_TextureData || !m_BackendTexture)
|
|
{
|
|
ResetBackendTexture(nullptr, m_TextureManager->GetErrorTexture()->GetBackendTexture());
|
|
m_State = UPLOADED;
|
|
return;
|
|
}
|
|
|
|
m_UploadedSize = 0;
|
|
for (uint32_t textureDataLevel = m_BaseLevelOffset, level = 0; textureDataLevel < m_TextureData->GetMIPLevels().size(); ++textureDataLevel)
|
|
{
|
|
const Tex::MIPLevel& levelData = m_TextureData->GetMIPLevels()[textureDataLevel];
|
|
deviceCommandContext->UploadTexture(m_BackendTexture.get(), m_BackendTexture->GetFormat(),
|
|
levelData.data, levelData.dataSize, level++);
|
|
m_UploadedSize += levelData.dataSize;
|
|
}
|
|
m_TextureData.reset();
|
|
|
|
m_State = UPLOADED;
|
|
}
|
|
|
|
Renderer::Backend::ITexture* CTexture::GetBackendTexture()
|
|
{
|
|
return m_BackendTexture && IsUploaded() ? m_BackendTexture.get() : m_FallbackBackendTexture;
|
|
}
|
|
|
|
const Renderer::Backend::ITexture* CTexture::GetBackendTexture() const
|
|
{
|
|
return m_BackendTexture && IsUploaded() ? m_BackendTexture.get() : m_FallbackBackendTexture;
|
|
}
|
|
|
|
bool CTexture::TryLoad()
|
|
{
|
|
// If we haven't started loading, then try loading, and if that fails then request conversion.
|
|
// If we have already tried prefetch loading, and it failed, bump the conversion request to HIGH priority.
|
|
if (m_State == UNLOADED || m_State == PREFETCH_NEEDS_LOADING || m_State == PREFETCH_NEEDS_CONVERTING)
|
|
{
|
|
if (std::shared_ptr<CTexture> self = m_Self.lock())
|
|
{
|
|
if (m_State != PREFETCH_NEEDS_CONVERTING && m_TextureManager->TryLoadingCached(self))
|
|
m_State = LOADED;
|
|
else
|
|
m_State = HIGH_NEEDS_CONVERTING;
|
|
}
|
|
}
|
|
|
|
return IsLoaded() || IsUploaded();
|
|
}
|
|
|
|
void CTexture::Prefetch()
|
|
{
|
|
if (m_State == UNLOADED)
|
|
{
|
|
if (std::shared_ptr<CTexture> self = m_Self.lock())
|
|
{
|
|
m_State = PREFETCH_NEEDS_LOADING;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CTexture::ResetBackendTexture(
|
|
std::unique_ptr<Renderer::Backend::ITexture> backendTexture,
|
|
Renderer::Backend::ITexture* fallbackBackendTexture)
|
|
{
|
|
m_BackendTexture = std::move(backendTexture);
|
|
m_FallbackBackendTexture = fallbackBackendTexture;
|
|
}
|
|
|
|
size_t CTexture::GetWidth() const
|
|
{
|
|
return GetBackendTexture()->GetWidth();
|
|
}
|
|
|
|
size_t CTexture::GetHeight() const
|
|
{
|
|
return GetBackendTexture()->GetHeight();
|
|
}
|
|
|
|
bool CTexture::HasAlpha() const
|
|
{
|
|
const Renderer::Backend::Format format = GetBackendTexture()->GetFormat();
|
|
return
|
|
format == Renderer::Backend::Format::A8_UNORM ||
|
|
format == Renderer::Backend::Format::R8G8B8A8_UNORM ||
|
|
format == Renderer::Backend::Format::BC1_RGBA_UNORM ||
|
|
format == Renderer::Backend::Format::BC2_UNORM ||
|
|
format == Renderer::Backend::Format::BC3_UNORM;
|
|
}
|
|
|
|
u32 CTexture::GetBaseColor() const
|
|
{
|
|
return m_BaseColor;
|
|
}
|
|
|
|
size_t CTexture::GetUploadedSize() const
|
|
{
|
|
return m_UploadedSize;
|
|
}
|
|
|
|
// CTextureManager: forward all calls to impl:
|
|
|
|
CTextureManager::CTextureManager(PIVFS vfs, bool highQuality, Renderer::Backend::IDevice* device) :
|
|
m(new CTextureManagerImpl(vfs, highQuality, device))
|
|
{
|
|
}
|
|
|
|
CTextureManager::~CTextureManager()
|
|
{
|
|
delete m;
|
|
}
|
|
|
|
CTexturePtr CTextureManager::CreateTexture(const CTextureProperties& props)
|
|
{
|
|
return m->CreateTexture(props);
|
|
}
|
|
|
|
CTexturePtr CTextureManager::WrapBackendTexture(
|
|
std::unique_ptr<Renderer::Backend::ITexture> backendTexture)
|
|
{
|
|
return m->WrapBackendTexture(std::move(backendTexture));
|
|
}
|
|
|
|
bool CTextureManager::TextureExists(const VfsPath& path) const
|
|
{
|
|
return m->TextureExists(path);
|
|
}
|
|
|
|
const CTexturePtr& CTextureManager::GetErrorTexture()
|
|
{
|
|
return m->GetErrorTexture();
|
|
}
|
|
|
|
const CTexturePtr& CTextureManager::GetWhiteTexture()
|
|
{
|
|
return m->GetWhiteTexture();
|
|
}
|
|
|
|
const CTexturePtr& CTextureManager::GetTransparentTexture()
|
|
{
|
|
return m->GetTransparentTexture();
|
|
}
|
|
|
|
const CTexturePtr& CTextureManager::GetAlphaGradientTexture()
|
|
{
|
|
return m->GetAlphaGradientTexture();
|
|
}
|
|
|
|
const CTexturePtr& CTextureManager::GetBlackTextureCube()
|
|
{
|
|
return m->GetBlackTextureCube();
|
|
}
|
|
|
|
bool CTextureManager::MakeProgress()
|
|
{
|
|
return m->MakeProgress();
|
|
}
|
|
|
|
bool CTextureManager::MakeUploadProgress(
|
|
Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
|
|
{
|
|
return m->MakeUploadProgress(deviceCommandContext);
|
|
}
|
|
|
|
bool CTextureManager::GenerateCachedTexture(const VfsPath& path, VfsPath& outputPath)
|
|
{
|
|
return m->GenerateCachedTexture(path, outputPath);
|
|
}
|
|
|
|
size_t CTextureManager::GetBytesUploaded() const
|
|
{
|
|
return m->GetBytesUploaded();
|
|
}
|
|
|
|
void CTextureManager::OnQualityChanged()
|
|
{
|
|
m->OnQualityChanged();
|
|
}
|