- Fixed issue with DXT file formats in D3D9.
- Added cube maps support to PVR file loader. - Added support for load cube maps directly via IVideoDriver::getTexture call. git-svn-id: svn://svn.code.sf.net/p/irrlicht/code/trunk@5259 dfc29bdd-3216-0410-991c-e03cc46cb475master
parent
deacb5b919
commit
d72c407efa
|
@ -16,6 +16,16 @@ namespace irr
|
|||
namespace video
|
||||
{
|
||||
|
||||
//! Enumeration describing the type of ITexture.
|
||||
enum E_TEXTURE_TYPE
|
||||
{
|
||||
//! 2D texture.
|
||||
ETT_2D,
|
||||
|
||||
//! Cubemap texture.
|
||||
ETT_CUBEMAP
|
||||
};
|
||||
|
||||
//! Interface for software image data.
|
||||
/** Image loaders create these images from files. IVideoDrivers convert
|
||||
these images into their (hardware) textures.
|
||||
|
|
|
@ -47,8 +47,9 @@ public:
|
|||
|
||||
//! Creates a multiple surfaces from the file eg. whole cube map.
|
||||
/** \param file File handle to check.
|
||||
\param type Pointer to E_TEXTURE_TYPE where a recommended type of the texture will be stored.
|
||||
\return Array of pointers to newly created images. */
|
||||
virtual core::array<IImage*> IImageLoader::loadImages(io::IReadFile* file) const
|
||||
virtual core::array<IImage*> IImageLoader::loadImages(io::IReadFile* file, E_TEXTURE_TYPE* type) const
|
||||
{
|
||||
core::array<IImage*> image;
|
||||
|
||||
|
|
|
@ -92,16 +92,6 @@ enum E_TEXTURE_LOCK_MODE
|
|||
ETLM_WRITE_ONLY
|
||||
};
|
||||
|
||||
//! Enumeration describing the type of ITexture.
|
||||
enum E_TEXTURE_TYPE
|
||||
{
|
||||
//! 2D texture.
|
||||
ETT_2D,
|
||||
|
||||
//! Cubemap texture.
|
||||
ETT_CUBEMAP
|
||||
};
|
||||
|
||||
//! Where did the last IVideoDriver::getTexture call find this texture
|
||||
enum E_TEXTURE_SOURCE
|
||||
{
|
||||
|
|
|
@ -1189,20 +1189,22 @@ namespace video
|
|||
method is useful for example if you want to read a heightmap
|
||||
for a terrain renderer.
|
||||
\param filename Name of the file from which the images are created.
|
||||
\param type Pointer to E_TEXTURE_TYPE where a recommended type of the texture will be stored.
|
||||
\return The array of created images.
|
||||
If you no longer need those images, you should call IImage::drop() on each of them.
|
||||
See IReferenceCounted::drop() for more information. */
|
||||
virtual core::array<IImage*> createImagesFromFile(const io::path& filename) = 0;
|
||||
virtual core::array<IImage*> createImagesFromFile(const io::path& filename, E_TEXTURE_TYPE* type = 0) = 0;
|
||||
|
||||
//! Creates a software images from a file.
|
||||
/** No hardware texture will be created for those images. This
|
||||
method is useful for example if you want to read a heightmap
|
||||
for a terrain renderer.
|
||||
\param file File from which the image is created.
|
||||
\param type Pointer to E_TEXTURE_TYPE where a recommended type of the texture will be stored.
|
||||
\return The array of created images.
|
||||
If you no longer need those images, you should call IImage::drop() on each of them.
|
||||
See IReferenceCounted::drop() for more information. */
|
||||
virtual core::array<IImage*> createImagesFromFile(io::IReadFile* file) = 0;
|
||||
virtual core::array<IImage*> createImagesFromFile(io::IReadFile* file, E_TEXTURE_TYPE* type = 0) = 0;
|
||||
|
||||
//! Creates a software image from a file.
|
||||
/** No hardware texture will be created for this image. This
|
||||
|
|
|
@ -3375,6 +3375,16 @@ D3DFORMAT CD3D9Driver::getD3DFormatFromColorFormat(ECOLOR_FORMAT format) const
|
|||
return D3DFMT_R8G8B8;
|
||||
case ECF_A8R8G8B8:
|
||||
return D3DFMT_A8R8G8B8;
|
||||
case ECF_DXT1:
|
||||
return D3DFMT_DXT1;
|
||||
case ECF_DXT2:
|
||||
return D3DFMT_DXT2;
|
||||
case ECF_DXT3:
|
||||
return D3DFMT_DXT3;
|
||||
case ECF_DXT4:
|
||||
return D3DFMT_DXT4;
|
||||
case ECF_DXT5:
|
||||
return D3DFMT_DXT5;
|
||||
case ECF_R16F:
|
||||
return D3DFMT_R16F;
|
||||
case ECF_G16R16F:
|
||||
|
|
|
@ -264,8 +264,8 @@ void CD3D9Texture::regenerateMipMapLevels(void* data, u32 layer)
|
|||
|
||||
if (data)
|
||||
{
|
||||
u32 width = Size.Width >> layer;
|
||||
u32 height = Size.Height >> layer;
|
||||
u32 width = Size.Width;
|
||||
u32 height = Size.Height;
|
||||
u8* tmpData = static_cast<u8*>(data);
|
||||
u32 dataSize = 0;
|
||||
u32 level = 0;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2013-2015 Patryk Nadrowski
|
||||
// Copyright (C) 2013-2016 Patryk Nadrowski
|
||||
// This file is part of the "Irrlicht Engine".
|
||||
// For conditions of distribution and use, see copyright notice in irrlicht.h
|
||||
|
||||
|
@ -13,19 +13,14 @@
|
|||
|
||||
namespace irr
|
||||
{
|
||||
|
||||
namespace video
|
||||
{
|
||||
|
||||
//! returns true if the file maybe is able to be loaded by this class
|
||||
//! based on the file extension (e.g. ".tga")
|
||||
bool CImageLoaderPVR::isALoadableFileExtension(const io::path& filename) const
|
||||
{
|
||||
return core::hasFileExtension(filename, "pvr");
|
||||
}
|
||||
|
||||
|
||||
//! returns true if the file maybe is able to be loaded by this class
|
||||
bool CImageLoaderPVR::isALoadableFileFormat(io::IReadFile* file) const
|
||||
{
|
||||
if (!file)
|
||||
|
@ -37,7 +32,6 @@ bool CImageLoaderPVR::isALoadableFileFormat(io::IReadFile* file) const
|
|||
|
||||
/*if (header.Version == 0x03525650) // TO-DO - fix endiannes
|
||||
{
|
||||
printf("Bad endian2\n");
|
||||
fourCC[0] = os::Byteswap::byteswap(fourCC[0]);
|
||||
fourCC[1] = os::Byteswap::byteswap(fourCC[1]);
|
||||
fourCC[2] = os::Byteswap::byteswap(fourCC[2]);
|
||||
|
@ -47,15 +41,35 @@ bool CImageLoaderPVR::isALoadableFileFormat(io::IReadFile* file) const
|
|||
return (fourCC[0] == 'P' && fourCC[1] == 'V' && fourCC[2] == 'R');
|
||||
}
|
||||
|
||||
|
||||
//! creates a surface from the file
|
||||
IImage* CImageLoaderPVR::loadImage(io::IReadFile* file) const
|
||||
{
|
||||
core::array<IImage*> imageArray = loadImages(file, 0);
|
||||
|
||||
const u32 imageCount = imageArray.size();
|
||||
|
||||
for (u32 i = 1; i < imageCount; ++i)
|
||||
{
|
||||
if (imageArray[i])
|
||||
imageArray[i]->drop();
|
||||
}
|
||||
|
||||
if (imageCount > 1)
|
||||
imageArray.erase(1, imageCount - 1);
|
||||
|
||||
return (imageCount > 1) ? imageArray[0] : 0;
|
||||
}
|
||||
|
||||
core::array<IImage*> CImageLoaderPVR::loadImages(io::IReadFile* file, E_TEXTURE_TYPE* type) const
|
||||
{
|
||||
// TO-DO -> use 'move' feature from C++11 standard.
|
||||
|
||||
SPVRHeader header;
|
||||
IImage* image = 0;
|
||||
|
||||
core::array<IImage*> imageArray;
|
||||
core::array<u8*> mipMapsDataArray;
|
||||
|
||||
ECOLOR_FORMAT format = ECF_UNKNOWN;
|
||||
u32 dataSize = 0;
|
||||
u32 mipMapsDataSize = 0;
|
||||
|
||||
file->seek(0);
|
||||
file->read(&header, sizeof(SPVRHeader));
|
||||
|
@ -93,102 +107,144 @@ IImage* CImageLoaderPVR::loadImage(io::IReadFile* file) const
|
|||
}
|
||||
else // Compressed texture formats
|
||||
{
|
||||
switch(header.PixelFormat)
|
||||
switch (header.PixelFormat)
|
||||
{
|
||||
case 0: // PVRTC 2bpp RGB
|
||||
format = ECF_PVRTC_RGB2;
|
||||
break;
|
||||
case 1: // PVRTC 2bpp RGBA
|
||||
format = ECF_PVRTC_ARGB2;
|
||||
break;
|
||||
case 2: // PVRTC 4bpp RGB
|
||||
format = ECF_PVRTC_RGB4;
|
||||
break;
|
||||
case 3: // PVRTC 4bpp RGBA
|
||||
format = ECF_PVRTC_ARGB4;
|
||||
break;
|
||||
case 4: // PVRTC-II 2bpp
|
||||
format = ECF_PVRTC2_ARGB2;
|
||||
break;
|
||||
case 5: // PVRTC-II 4bpp
|
||||
format = ECF_PVRTC2_ARGB4;
|
||||
break;
|
||||
case 6: // ETC1
|
||||
format = ECF_ETC1;
|
||||
break;
|
||||
case 7: // DXT1 / BC1
|
||||
format = ECF_DXT1;
|
||||
break;
|
||||
case 8: // DXT2
|
||||
case 9: // DXT3 / BC2
|
||||
format = ECF_DXT3;
|
||||
break;
|
||||
case 10: // DXT4
|
||||
case 11: // DXT5 / BC3
|
||||
format = ECF_DXT5;
|
||||
break;
|
||||
case 22: // ETC2 RGB
|
||||
format = ECF_ETC2_RGB;
|
||||
break;
|
||||
case 23: // ETC2 RGBA
|
||||
format = ECF_ETC2_ARGB;
|
||||
break;
|
||||
default:
|
||||
format = ECF_UNKNOWN;
|
||||
break;
|
||||
case 0: // PVRTC 2bpp RGB
|
||||
format = ECF_PVRTC_RGB2;
|
||||
break;
|
||||
case 1: // PVRTC 2bpp RGBA
|
||||
format = ECF_PVRTC_ARGB2;
|
||||
break;
|
||||
case 2: // PVRTC 4bpp RGB
|
||||
format = ECF_PVRTC_RGB4;
|
||||
break;
|
||||
case 3: // PVRTC 4bpp RGBA
|
||||
format = ECF_PVRTC_ARGB4;
|
||||
break;
|
||||
case 4: // PVRTC-II 2bpp
|
||||
format = ECF_PVRTC2_ARGB2;
|
||||
break;
|
||||
case 5: // PVRTC-II 4bpp
|
||||
format = ECF_PVRTC2_ARGB4;
|
||||
break;
|
||||
case 6: // ETC1
|
||||
format = ECF_ETC1;
|
||||
break;
|
||||
case 7: // DXT1 / BC1
|
||||
format = ECF_DXT1;
|
||||
break;
|
||||
case 8: // DXT2
|
||||
case 9: // DXT3 / BC2
|
||||
format = ECF_DXT3;
|
||||
break;
|
||||
case 10: // DXT4
|
||||
case 11: // DXT5 / BC3
|
||||
format = ECF_DXT5;
|
||||
break;
|
||||
case 22: // ETC2 RGB
|
||||
format = ECF_ETC2_RGB;
|
||||
break;
|
||||
case 23: // ETC2 RGBA
|
||||
format = ECF_ETC2_ARGB;
|
||||
break;
|
||||
default:
|
||||
format = ECF_UNKNOWN;
|
||||
break;
|
||||
}
|
||||
|
||||
if (format != ECF_UNKNOWN)
|
||||
{
|
||||
// 3D textures, texture arrays, cube maps textures aren't currently supported
|
||||
if (header.Depth < 2 && header.NumSurfaces < 2 && header.NumFaces < 2)
|
||||
imageArray.set_used(1);
|
||||
E_TEXTURE_TYPE tmpType = ETT_2D;
|
||||
|
||||
// check for texture type
|
||||
|
||||
if (header.NumFaces == 6) // cube map
|
||||
{
|
||||
dataSize = IImage::getDataSizeFromFormat(format, header.Width, header.Height);
|
||||
imageArray.set_used(6);
|
||||
tmpType = ETT_CUBEMAP;
|
||||
|
||||
u8* data = new u8[dataSize];
|
||||
file->read(data, dataSize);
|
||||
imageArray.reallocate(6);
|
||||
}
|
||||
else if (header.Depth > 1) // 3d texture
|
||||
{
|
||||
// TO-DO
|
||||
}
|
||||
else if (header.NumSurfaces > 1) // texture array
|
||||
{
|
||||
// To-DO
|
||||
}
|
||||
|
||||
image = new CImage(format, core::dimension2d<u32>(header.Width, header.Height), data, true, true);
|
||||
if (type)
|
||||
*type = tmpType;
|
||||
|
||||
if (header.MipMapCount > 1)
|
||||
// prepare mipmaps data
|
||||
|
||||
dataSize = 0;
|
||||
|
||||
for (u32 i = 1; i < header.MipMapCount; ++i)
|
||||
{
|
||||
u32 tmpWidth = header.Width >> i;
|
||||
u32 tmpHeight = header.Height >> i;
|
||||
|
||||
dataSize += IImage::getDataSizeFromFormat(format, tmpWidth, tmpHeight);
|
||||
}
|
||||
|
||||
if (header.MipMapCount > 1)
|
||||
{
|
||||
for (u32 j = 0; j < imageArray.size(); ++j)
|
||||
mipMapsDataArray.push_back(new u8[dataSize]);
|
||||
}
|
||||
|
||||
// read texture
|
||||
|
||||
dataSize = 0;
|
||||
long offset = 0;
|
||||
|
||||
for (u32 i = 0; i < header.MipMapCount; ++i)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
u32 tmpWidth = header.Width;
|
||||
u32 tmpHeight = header.Height;
|
||||
|
||||
do
|
||||
for (u32 j = 0; j < imageArray.size(); ++j)
|
||||
{
|
||||
if (tmpWidth > 1)
|
||||
tmpWidth >>= 1;
|
||||
dataSize = IImage::getDataSizeFromFormat(format, header.Width, header.Height);
|
||||
|
||||
if (tmpHeight > 1)
|
||||
tmpHeight >>= 1;
|
||||
u8* data = new u8[dataSize];
|
||||
file->read(data, dataSize);
|
||||
|
||||
mipMapsDataSize += IImage::getDataSizeFromFormat(format, tmpWidth, tmpHeight);
|
||||
imageArray[j] = new CImage(format, core::dimension2d<u32>(header.Width, header.Height), data, true, true);
|
||||
}
|
||||
while (tmpWidth != 1 || tmpHeight != 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
u32 tmpWidth = header.Width >> i;
|
||||
u32 tmpHeight = header.Height >> i;
|
||||
|
||||
u8* mipMapsData = new u8[mipMapsDataSize];
|
||||
file->read(mipMapsData, mipMapsDataSize);
|
||||
dataSize = IImage::getDataSizeFromFormat(format, tmpWidth, tmpHeight);
|
||||
|
||||
image->setMipMapsData(mipMapsData, true, true);
|
||||
for (u32 j = 0; j < imageArray.size(); ++j)
|
||||
file->read(mipMapsDataArray[j] + offset, dataSize);
|
||||
|
||||
offset += dataSize;
|
||||
}
|
||||
}
|
||||
|
||||
// assign mipmaps data
|
||||
|
||||
for (u32 i = 0; i < mipMapsDataArray.size(); ++i)
|
||||
imageArray[i]->setMipMapsData(mipMapsDataArray[i], true, true);
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
return imageArray;
|
||||
}
|
||||
|
||||
|
||||
//! creates a loader which is able to load pvr images
|
||||
IImageLoader* createImageLoaderPVR()
|
||||
{
|
||||
return new CImageLoaderPVR();
|
||||
}
|
||||
|
||||
|
||||
} // end namespace video
|
||||
} // end namespace irr
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2013-2015 Patryk Nadrowski
|
||||
// Copyright (C) 2013-2016 Patryk Nadrowski
|
||||
// This file is part of the "Irrlicht Engine".
|
||||
// For conditions of distribution and use, see copyright notice in irrlicht.h
|
||||
|
||||
|
@ -16,10 +16,8 @@ namespace irr
|
|||
namespace video
|
||||
{
|
||||
|
||||
// byte-align structures
|
||||
#include "irrpack.h"
|
||||
|
||||
/* structures */
|
||||
struct SPVRHeader
|
||||
{
|
||||
u32 Version;
|
||||
|
@ -36,29 +34,22 @@ struct SPVRHeader
|
|||
u32 MetDataSize;
|
||||
} PACK_STRUCT;
|
||||
|
||||
// Default alignment
|
||||
#include "irrunpack.h"
|
||||
|
||||
/*!
|
||||
Surface Loader for PVR images
|
||||
*/
|
||||
class CImageLoaderPVR : public IImageLoader
|
||||
{
|
||||
public:
|
||||
|
||||
//! returns true if the file maybe is able to be loaded by this class
|
||||
//! based on the file extension (e.g. ".tga")
|
||||
virtual bool isALoadableFileExtension(const io::path& filename) const;
|
||||
|
||||
//! returns true if the file maybe is able to be loaded by this class
|
||||
virtual bool isALoadableFileFormat(io::IReadFile* file) const;
|
||||
|
||||
//! creates a surface from the file
|
||||
virtual IImage* loadImage(io::IReadFile* file) const;
|
||||
|
||||
virtual core::array<IImage*> loadImages(io::IReadFile* file, E_TEXTURE_TYPE* type) const;
|
||||
};
|
||||
|
||||
} // end namespace video
|
||||
} // end namespace irr
|
||||
}
|
||||
}
|
||||
|
||||
#endif // compiled with PVR loader
|
||||
#endif
|
||||
#endif
|
||||
|
|
|
@ -493,7 +493,7 @@ ITexture* CNullDriver::addTextureCubemap(const io::path& name, IImage* imagePosX
|
|||
|
||||
ITexture* t = 0;
|
||||
|
||||
core::array<IImage*> imageArray(1);
|
||||
core::array<IImage*> imageArray(6);
|
||||
imageArray.push_back(imagePosX);
|
||||
imageArray.push_back(imageNegX);
|
||||
imageArray.push_back(imagePosY);
|
||||
|
@ -614,20 +614,37 @@ ITexture* CNullDriver::getTexture(io::IReadFile* file)
|
|||
video::ITexture* CNullDriver::loadTextureFromFile(io::IReadFile* file, const io::path& hashName )
|
||||
{
|
||||
ITexture* texture = 0;
|
||||
IImage* image = createImageFromFile(file);
|
||||
|
||||
if (image)
|
||||
E_TEXTURE_TYPE type = ETT_2D;
|
||||
|
||||
core::array<IImage*> imageArray = createImagesFromFile(file, &type);
|
||||
|
||||
if (checkImage(imageArray))
|
||||
{
|
||||
core::array<IImage*> imageArray(1);
|
||||
imageArray.push_back(image);
|
||||
|
||||
if (checkImage(imageArray))
|
||||
switch (type)
|
||||
{
|
||||
texture = createDeviceDependentTexture(hashName.size() ? hashName : file->getFileName(), image);
|
||||
case ETT_2D:
|
||||
texture = createDeviceDependentTexture(hashName.size() ? hashName : file->getFileName(), imageArray[0]);
|
||||
break;
|
||||
case ETT_CUBEMAP:
|
||||
if (imageArray.size() >= 6 && imageArray[0] && imageArray[1] && imageArray[2] && imageArray[3] && imageArray[4] && imageArray[5])
|
||||
{
|
||||
texture = createDeviceDependentTextureCubemap(hashName.size() ? hashName : file->getFileName(), imageArray);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
_IRR_DEBUG_BREAK_IF(true);
|
||||
break;
|
||||
}
|
||||
|
||||
os::Printer::log("Loaded texture", file->getFileName());
|
||||
image->drop();
|
||||
|
||||
if (texture)
|
||||
os::Printer::log("Loaded texture", file->getFileName());
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < imageArray.size(); ++i)
|
||||
{
|
||||
if (imageArray[i])
|
||||
imageArray[i]->drop();
|
||||
}
|
||||
|
||||
return texture;
|
||||
|
@ -1481,7 +1498,7 @@ bool CNullDriver::getTextureCreationFlag(E_TEXTURE_CREATION_FLAG flag) const
|
|||
return (TextureCreationFlags & flag)!=0;
|
||||
}
|
||||
|
||||
core::array<IImage*> CNullDriver::createImagesFromFile(const io::path& filename)
|
||||
core::array<IImage*> CNullDriver::createImagesFromFile(const io::path& filename, E_TEXTURE_TYPE* type)
|
||||
{
|
||||
// TO-DO -> use 'move' feature from C++11 standard.
|
||||
|
||||
|
@ -1493,7 +1510,7 @@ core::array<IImage*> CNullDriver::createImagesFromFile(const io::path& filename)
|
|||
|
||||
if (file)
|
||||
{
|
||||
imageArray = createImagesFromFile(file);
|
||||
imageArray = createImagesFromFile(file, type);
|
||||
file->drop();
|
||||
}
|
||||
else
|
||||
|
@ -1503,7 +1520,7 @@ core::array<IImage*> CNullDriver::createImagesFromFile(const io::path& filename)
|
|||
return imageArray;
|
||||
}
|
||||
|
||||
core::array<IImage*> CNullDriver::createImagesFromFile(io::IReadFile* file)
|
||||
core::array<IImage*> CNullDriver::createImagesFromFile(io::IReadFile* file, E_TEXTURE_TYPE* type)
|
||||
{
|
||||
// TO-DO -> use 'move' feature from C++11 standard.
|
||||
|
||||
|
@ -1520,7 +1537,7 @@ core::array<IImage*> CNullDriver::createImagesFromFile(io::IReadFile* file)
|
|||
{
|
||||
// reset file position which might have changed due to previous loadImage calls
|
||||
file->seek(0);
|
||||
imageArray = SurfaceLoader[i]->loadImages(file);
|
||||
imageArray = SurfaceLoader[i]->loadImages(file, type);
|
||||
|
||||
if (imageArray.size() == 0)
|
||||
{
|
||||
|
@ -1544,7 +1561,7 @@ core::array<IImage*> CNullDriver::createImagesFromFile(io::IReadFile* file)
|
|||
if (SurfaceLoader[i]->isALoadableFileFormat(file))
|
||||
{
|
||||
file->seek(0);
|
||||
imageArray = SurfaceLoader[i]->loadImages(file);
|
||||
imageArray = SurfaceLoader[i]->loadImages(file, type);
|
||||
|
||||
if (imageArray.size() == 0)
|
||||
{
|
||||
|
|
|
@ -341,9 +341,9 @@ namespace video
|
|||
//! Returns if a texture creation flag is enabled or disabled.
|
||||
virtual bool getTextureCreationFlag(E_TEXTURE_CREATION_FLAG flag) const _IRR_OVERRIDE_;
|
||||
|
||||
virtual core::array<IImage*> createImagesFromFile(const io::path& filename) _IRR_OVERRIDE_;
|
||||
virtual core::array<IImage*> createImagesFromFile(const io::path& filename, E_TEXTURE_TYPE* type = 0) _IRR_OVERRIDE_;
|
||||
|
||||
virtual core::array<IImage*> createImagesFromFile(io::IReadFile* file) _IRR_OVERRIDE_;
|
||||
virtual core::array<IImage*> createImagesFromFile(io::IReadFile* file, E_TEXTURE_TYPE* type = 0) _IRR_OVERRIDE_;
|
||||
|
||||
//! Creates a software image from a byte array.
|
||||
/** \param useForeignMemory: If true, the image will use the data pointer
|
||||
|
|
Loading…
Reference in New Issue