Add GEVulkanTextureDescriptor to avoid refilling every frame
This commit is contained in:
parent
3769c18288
commit
713ab53272
@ -38,6 +38,7 @@ set(GE_SOURCES
|
||||
src/ge_vulkan_scene_manager.cpp
|
||||
src/ge_vulkan_shader_manager.cpp
|
||||
src/ge_vulkan_texture.cpp
|
||||
src/ge_vulkan_texture_descriptor.cpp
|
||||
src/ge_gl_texture.cpp
|
||||
src/ge_spm.cpp
|
||||
)
|
||||
|
104
lib/graphics_engine/include/ge_vulkan_texture_descriptor.hpp
Normal file
104
lib/graphics_engine/include/ge_vulkan_texture_descriptor.hpp
Normal file
@ -0,0 +1,104 @@
|
||||
#ifndef HEADER_GE_VULKAN_TEXTURE_DESCRIPTOR_HPP
|
||||
#define HEADER_GE_VULKAN_TEXTURE_DESCRIPTOR_HPP
|
||||
|
||||
#include "vulkan_wrapper.h"
|
||||
|
||||
#include "IrrCompileConfig.h"
|
||||
namespace irr
|
||||
{
|
||||
namespace video { class ITexture; }
|
||||
}
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace GE
|
||||
{
|
||||
class GEVulkanDriver;
|
||||
enum GEVulkanSampler : unsigned;
|
||||
|
||||
class GEVulkanTextureDescriptor
|
||||
{
|
||||
typedef std::array<std::shared_ptr<VkImageView>,
|
||||
_IRR_MATERIAL_MAX_TEXTURES_> TextureList;
|
||||
|
||||
std::map<TextureList, int> m_texture_list;
|
||||
|
||||
std::shared_ptr<VkImageView> m_white_image, m_transparent_image;
|
||||
|
||||
VkDescriptorSetLayout m_descriptor_set_layout;
|
||||
|
||||
VkDescriptorPool m_descriptor_pool;
|
||||
|
||||
std::vector<VkDescriptorSet> m_descriptor_sets;
|
||||
|
||||
const unsigned m_max_texture_list;
|
||||
|
||||
const unsigned m_max_layer;
|
||||
|
||||
const unsigned m_binding;
|
||||
|
||||
GEVulkanSampler m_sampler_use;
|
||||
|
||||
GEVulkanDriver* m_vk;
|
||||
|
||||
bool m_recreate_next_frame;
|
||||
|
||||
bool m_needs_update_descriptor;
|
||||
public:
|
||||
// ------------------------------------------------------------------------
|
||||
GEVulkanTextureDescriptor(unsigned max_texture_list, unsigned max_layer,
|
||||
bool single_descriptor, unsigned binding = 0);
|
||||
// ------------------------------------------------------------------------
|
||||
~GEVulkanTextureDescriptor();
|
||||
// ------------------------------------------------------------------------
|
||||
void handleDeletedTextures()
|
||||
{
|
||||
bool has_deleted_image_view = false;
|
||||
for (auto& p : m_texture_list)
|
||||
{
|
||||
for (auto& t : p.first)
|
||||
{
|
||||
if (*(t.get()) == VK_NULL_HANDLE)
|
||||
{
|
||||
has_deleted_image_view = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (has_deleted_image_view || m_recreate_next_frame)
|
||||
{
|
||||
m_texture_list.clear();
|
||||
m_needs_update_descriptor = true;
|
||||
m_recreate_next_frame = false;
|
||||
}
|
||||
}
|
||||
// ------------------------------------------------------------------------
|
||||
int getTextureID(const irr::video::ITexture** list);
|
||||
// ------------------------------------------------------------------------
|
||||
void setSamplerUse(GEVulkanSampler sampler)
|
||||
{
|
||||
if (m_sampler_use == sampler)
|
||||
return;
|
||||
m_sampler_use = sampler;
|
||||
m_needs_update_descriptor = true;
|
||||
}
|
||||
// ------------------------------------------------------------------------
|
||||
void updateDescriptor();
|
||||
// ------------------------------------------------------------------------
|
||||
unsigned getMaxTextureList() const { return m_max_texture_list; }
|
||||
// ------------------------------------------------------------------------
|
||||
unsigned getMaxLayer() const { return m_max_layer; }
|
||||
// ------------------------------------------------------------------------
|
||||
VkDescriptorSetLayout* getDescriptorSetLayout()
|
||||
{ return &m_descriptor_set_layout; }
|
||||
// ------------------------------------------------------------------------
|
||||
VkDescriptorSet* getDescriptorSet()
|
||||
{ return m_descriptor_sets.data(); }
|
||||
}; // GEVulkanTextureDescriptor
|
||||
|
||||
}
|
||||
|
||||
#endif
|
@ -13,7 +13,6 @@ GEVulkanDepthTexture::GEVulkanDepthTexture(GEVulkanDriver* vk,
|
||||
m_vulkan_device = m_vk->getDevice();
|
||||
m_image = VK_NULL_HANDLE;
|
||||
m_vma_allocation = VK_NULL_HANDLE;
|
||||
m_image_view = VK_NULL_HANDLE;
|
||||
m_has_mipmaps = false;
|
||||
m_locked_data = NULL;
|
||||
m_size = m_orig_size = m_max_size = size;
|
||||
|
@ -38,7 +38,8 @@ public:
|
||||
// ------------------------------------------------------------------------
|
||||
virtual void regenerateMipMapLevels(void* mipmap_data = NULL) {}
|
||||
// ------------------------------------------------------------------------
|
||||
virtual u64 getTextureHandler() const { return (u64)m_image_view; }
|
||||
virtual u64 getTextureHandler() const
|
||||
{ return (u64)*(m_image_view.get()); }
|
||||
// ------------------------------------------------------------------------
|
||||
virtual unsigned int getTextureSize() const { return m_texture_size; }
|
||||
// ------------------------------------------------------------------------
|
||||
@ -46,6 +47,9 @@ public:
|
||||
// ------------------------------------------------------------------------
|
||||
virtual void updateTexture(void* data, irr::video::ECOLOR_FORMAT format,
|
||||
u32 w, u32 h, u32 x, u32 y) {}
|
||||
// ------------------------------------------------------------------------
|
||||
virtual std::shared_ptr<VkImageView> getImageView() const
|
||||
{ return m_image_view; }
|
||||
}; // GEVulkanDepthTexture
|
||||
|
||||
}
|
||||
|
@ -24,8 +24,8 @@ GEVulkanTexture::GEVulkanTexture(const std::string& path,
|
||||
m_locked_data(NULL),
|
||||
m_vulkan_device(getVKDriver()->getDevice()),
|
||||
m_image(VK_NULL_HANDLE), m_vma_allocation(VK_NULL_HANDLE),
|
||||
m_image_view(VK_NULL_HANDLE), m_texture_size(0),
|
||||
m_disable_reload(false), m_has_mipmaps(true),
|
||||
m_texture_size(0), m_disable_reload(false),
|
||||
m_has_mipmaps(true),
|
||||
m_internal_format(VK_FORMAT_R8G8B8A8_UNORM),
|
||||
m_vk(getVKDriver())
|
||||
{
|
||||
@ -50,8 +50,8 @@ GEVulkanTexture::GEVulkanTexture(video::IImage* img, const std::string& name)
|
||||
m_locked_data(NULL),
|
||||
m_vulkan_device(getVKDriver()->getDevice()),
|
||||
m_image(VK_NULL_HANDLE), m_vma_allocation(VK_NULL_HANDLE),
|
||||
m_image_view(VK_NULL_HANDLE), m_texture_size(0),
|
||||
m_disable_reload(true), m_has_mipmaps(true),
|
||||
m_texture_size(0), m_disable_reload(true),
|
||||
m_has_mipmaps(true),
|
||||
m_internal_format(VK_FORMAT_R8G8B8A8_UNORM),
|
||||
m_vk(getVKDriver())
|
||||
{
|
||||
@ -76,8 +76,7 @@ GEVulkanTexture::GEVulkanTexture(const std::string& name, unsigned int size,
|
||||
: video::ITexture(name.c_str()), m_image_mani(nullptr),
|
||||
m_locked_data(NULL), m_vulkan_device(getVKDriver()->getDevice()),
|
||||
m_image(VK_NULL_HANDLE), m_vma_allocation(VK_NULL_HANDLE),
|
||||
m_image_view(VK_NULL_HANDLE), m_texture_size(0),
|
||||
m_disable_reload(true), m_has_mipmaps(true),
|
||||
m_texture_size(0), m_disable_reload(true), m_has_mipmaps(true),
|
||||
m_internal_format(single_channel ?
|
||||
VK_FORMAT_R8_UNORM : VK_FORMAT_R8G8B8A8_UNORM),
|
||||
m_vk(getVKDriver())
|
||||
@ -102,7 +101,7 @@ GEVulkanTexture::~GEVulkanTexture()
|
||||
m_image_view_lock.lock();
|
||||
m_image_view_lock.unlock();
|
||||
|
||||
if (m_image_view != VK_NULL_HANDLE || m_image != VK_NULL_HANDLE ||
|
||||
if (m_image_view || m_image != VK_NULL_HANDLE ||
|
||||
m_vma_allocation != VK_NULL_HANDLE)
|
||||
m_vk->waitIdle();
|
||||
|
||||
@ -364,16 +363,26 @@ bool GEVulkanTexture::createImageView(VkImageAspectFlags aspect_flags)
|
||||
view_info.components.a = VK_COMPONENT_SWIZZLE_R;
|
||||
}
|
||||
|
||||
std::shared_ptr<VkImageView> image_view = std::make_shared<VkImageView>();
|
||||
VkResult result = vkCreateImageView(m_vulkan_device, &view_info, NULL,
|
||||
&m_image_view);
|
||||
return (result == VK_SUCCESS);
|
||||
image_view.get());
|
||||
if (result == VK_SUCCESS)
|
||||
{
|
||||
m_image_view = image_view;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} // createImageView
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
void GEVulkanTexture::clearVulkanData()
|
||||
{
|
||||
if (m_image_view != VK_NULL_HANDLE)
|
||||
vkDestroyImageView(m_vulkan_device, m_image_view, NULL);
|
||||
if (m_image_view)
|
||||
{
|
||||
vkDestroyImageView(m_vulkan_device, *m_image_view.get(), NULL);
|
||||
*(m_image_view.get()) = VK_NULL_HANDLE;
|
||||
m_image_view.reset();
|
||||
}
|
||||
if (m_image != VK_NULL_HANDLE)
|
||||
vmaDestroyImage(m_vk->getVmaAllocator(), m_image, m_vma_allocation);
|
||||
} // clearVulkanData
|
||||
@ -696,7 +705,7 @@ void GEVulkanTexture::reload()
|
||||
m_image_view_lock.lock();
|
||||
m_image_view_lock.unlock();
|
||||
|
||||
if (m_image_view != VK_NULL_HANDLE || m_image != VK_NULL_HANDLE ||
|
||||
if (m_image_view || m_image != VK_NULL_HANDLE ||
|
||||
m_vma_allocation != VK_NULL_HANDLE)
|
||||
m_vk->waitIdle();
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <ITexture.h>
|
||||
@ -33,7 +34,7 @@ protected:
|
||||
|
||||
VmaAllocation m_vma_allocation;
|
||||
|
||||
VkImageView m_image_view;
|
||||
std::shared_ptr<VkImageView> m_image_view;
|
||||
|
||||
unsigned int m_texture_size;
|
||||
|
||||
@ -165,7 +166,7 @@ public:
|
||||
{
|
||||
m_image_view_lock.lock();
|
||||
m_image_view_lock.unlock();
|
||||
return (u64)m_image_view;
|
||||
return m_image_view ? (u64)*(m_image_view.get()) : 0;
|
||||
}
|
||||
// ------------------------------------------------------------------------
|
||||
virtual unsigned int getTextureSize() const
|
||||
@ -180,6 +181,13 @@ public:
|
||||
virtual void updateTexture(void* data, irr::video::ECOLOR_FORMAT format,
|
||||
u32 w, u32 h, u32 x, u32 y);
|
||||
// ------------------------------------------------------------------------
|
||||
virtual std::shared_ptr<VkImageView> getImageView() const
|
||||
{
|
||||
m_image_view_lock.lock();
|
||||
m_image_view_lock.unlock();
|
||||
return m_image_view;
|
||||
}
|
||||
// ------------------------------------------------------------------------
|
||||
VkFormat getInternalFormat() const { return m_internal_format; }
|
||||
}; // GEVulkanTexture
|
||||
|
||||
|
228
lib/graphics_engine/src/ge_vulkan_texture_descriptor.cpp
Normal file
228
lib/graphics_engine/src/ge_vulkan_texture_descriptor.cpp
Normal file
@ -0,0 +1,228 @@
|
||||
#include "ge_vulkan_texture_descriptor.hpp"
|
||||
|
||||
#include "ge_main.hpp"
|
||||
#include "ge_vulkan_driver.hpp"
|
||||
#include "ge_vulkan_texture.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
|
||||
namespace GE
|
||||
{
|
||||
// ----------------------------------------------------------------------------
|
||||
GEVulkanTextureDescriptor::GEVulkanTextureDescriptor(unsigned max_texture_list,
|
||||
unsigned max_layer,
|
||||
bool single_descriptor,
|
||||
unsigned binding)
|
||||
: m_max_texture_list(max_texture_list),
|
||||
m_max_layer(max_layer), m_binding(binding)
|
||||
{
|
||||
if (m_max_layer > _IRR_MATERIAL_MAX_TEXTURES_)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Too large max_layer for GEVulkanTextureDescriptor");
|
||||
}
|
||||
|
||||
m_vk = getVKDriver();
|
||||
|
||||
// m_descriptor_set_layout
|
||||
VkDescriptorSetLayoutBinding texture_layout_binding = {};
|
||||
texture_layout_binding.binding = m_binding;
|
||||
texture_layout_binding.descriptorCount =
|
||||
single_descriptor ? m_max_texture_list * m_max_layer : m_max_layer;
|
||||
texture_layout_binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
texture_layout_binding.pImmutableSamplers = NULL;
|
||||
texture_layout_binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
|
||||
VkDescriptorSetLayoutCreateInfo setinfo = {};
|
||||
setinfo.flags = 0;
|
||||
setinfo.pNext = NULL;
|
||||
setinfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
|
||||
setinfo.pBindings = &texture_layout_binding;
|
||||
setinfo.bindingCount = 1;
|
||||
if (vkCreateDescriptorSetLayout(m_vk->getDevice(), &setinfo,
|
||||
NULL, &m_descriptor_set_layout) != VK_SUCCESS)
|
||||
{
|
||||
throw std::runtime_error("vkCreateDescriptorSetLayout failed for "
|
||||
"GEVulkanTextureDescriptor");
|
||||
}
|
||||
|
||||
// m_descriptor_pool
|
||||
VkDescriptorPoolSize pool_size;
|
||||
pool_size.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
pool_size.descriptorCount = m_max_texture_list * m_max_layer;
|
||||
|
||||
VkDescriptorPoolCreateInfo pool_info = {};
|
||||
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
pool_info.flags = 0;
|
||||
pool_info.maxSets = single_descriptor ? 1 : m_max_texture_list;
|
||||
pool_info.poolSizeCount = 1;
|
||||
pool_info.pPoolSizes = &pool_size;
|
||||
if (vkCreateDescriptorPool(m_vk->getDevice(), &pool_info, NULL,
|
||||
&m_descriptor_pool) != VK_SUCCESS)
|
||||
{
|
||||
throw std::runtime_error("vkCreateDescriptorPool failed for "
|
||||
"GEVulkanTextureDescriptor");
|
||||
}
|
||||
|
||||
// m_descriptor_sets
|
||||
if (single_descriptor)
|
||||
m_descriptor_sets.resize(1);
|
||||
else
|
||||
m_descriptor_sets.resize(m_max_texture_list);
|
||||
std::vector<VkDescriptorSetLayout> layouts(m_descriptor_sets.size(),
|
||||
m_descriptor_set_layout);
|
||||
|
||||
VkDescriptorSetAllocateInfo alloc_info = {};
|
||||
alloc_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
||||
alloc_info.descriptorPool = m_descriptor_pool;
|
||||
alloc_info.descriptorSetCount = layouts.size();
|
||||
alloc_info.pSetLayouts = layouts.data();
|
||||
|
||||
if (vkAllocateDescriptorSets(m_vk->getDevice(), &alloc_info,
|
||||
m_descriptor_sets.data()) != VK_SUCCESS)
|
||||
{
|
||||
throw std::runtime_error("vkAllocateDescriptorSets failed for "
|
||||
"GEVulkanTextureDescriptor");
|
||||
}
|
||||
|
||||
m_sampler_use = GVS_NEAREST;
|
||||
m_recreate_next_frame = false;
|
||||
m_needs_update_descriptor = false;
|
||||
|
||||
GEVulkanTexture* tex = static_cast<GEVulkanTexture*>(
|
||||
m_vk->getWhiteTexture());
|
||||
m_white_image = tex->getImageView();
|
||||
tex = static_cast<GEVulkanTexture*>(m_vk->getTransparentTexture());
|
||||
m_transparent_image = tex->getImageView();
|
||||
} // GEVulkanTextureDescriptor
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
GEVulkanTextureDescriptor::~GEVulkanTextureDescriptor()
|
||||
{
|
||||
vkDestroyDescriptorSetLayout(m_vk->getDevice(), m_descriptor_set_layout,
|
||||
NULL);
|
||||
vkDestroyDescriptorPool(m_vk->getDevice(), m_descriptor_pool, NULL);
|
||||
} // ~GEVulkanTextureDescriptor
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
void GEVulkanTextureDescriptor::updateDescriptor()
|
||||
{
|
||||
if (!m_needs_update_descriptor)
|
||||
return;
|
||||
m_needs_update_descriptor = false;
|
||||
if (m_texture_list.empty())
|
||||
return;
|
||||
|
||||
std::vector<VkDescriptorImageInfo> image_infos;
|
||||
image_infos.resize(m_texture_list.size() * m_max_layer);
|
||||
for (auto& p : m_texture_list)
|
||||
{
|
||||
const size_t max_size = std::min((size_t)m_max_layer, p.first.size());
|
||||
for (unsigned i = 0; i < max_size; i++)
|
||||
{
|
||||
VkDescriptorImageInfo info;
|
||||
info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
info.sampler = m_vk->getSampler(m_sampler_use);
|
||||
info.imageView = *(p.first[i].get());
|
||||
if (info.imageView == VK_NULL_HANDLE)
|
||||
info.imageView = *m_transparent_image.get();
|
||||
image_infos[p.second * m_max_layer + i] = info;
|
||||
}
|
||||
}
|
||||
|
||||
bool single_descriptor = (m_descriptor_sets.size() == 1);
|
||||
if (single_descriptor)
|
||||
{
|
||||
VkDescriptorImageInfo dummy_info;
|
||||
dummy_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
dummy_info.imageView = *m_transparent_image.get();
|
||||
dummy_info.sampler = m_vk->getSampler(m_sampler_use);
|
||||
image_infos.resize(m_max_texture_list * m_max_layer, dummy_info);
|
||||
}
|
||||
|
||||
m_vk->waitIdle();
|
||||
if (single_descriptor)
|
||||
{
|
||||
VkWriteDescriptorSet write_descriptor_set = {};
|
||||
write_descriptor_set.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
write_descriptor_set.dstBinding = m_binding;
|
||||
write_descriptor_set.dstArrayElement = 0;
|
||||
write_descriptor_set.descriptorType =
|
||||
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write_descriptor_set.descriptorCount = m_max_texture_list * m_max_layer;
|
||||
write_descriptor_set.pBufferInfo = 0;
|
||||
write_descriptor_set.dstSet = m_descriptor_sets[0];
|
||||
write_descriptor_set.pImageInfo = image_infos.data();
|
||||
|
||||
vkUpdateDescriptorSets(m_vk->getDevice(), 1, &write_descriptor_set, 0,
|
||||
NULL);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::vector<VkWriteDescriptorSet> all_sets;
|
||||
for (unsigned i = 0; i < image_infos.size(); i += m_max_layer)
|
||||
{
|
||||
const unsigned set_idx = i / m_max_layer;
|
||||
VkWriteDescriptorSet write_descriptor_set = {};
|
||||
write_descriptor_set.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
write_descriptor_set.dstBinding = m_binding;
|
||||
write_descriptor_set.dstArrayElement = 0;
|
||||
write_descriptor_set.descriptorType =
|
||||
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write_descriptor_set.descriptorCount = m_max_layer;
|
||||
write_descriptor_set.pBufferInfo = 0;
|
||||
write_descriptor_set.dstSet = m_descriptor_sets[set_idx];
|
||||
write_descriptor_set.pImageInfo = &image_infos[i];
|
||||
all_sets.push_back(write_descriptor_set);
|
||||
}
|
||||
vkUpdateDescriptorSets(m_vk->getDevice(), all_sets.size(),
|
||||
all_sets.data(), 0, NULL);
|
||||
}
|
||||
} // updateDescriptor
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
int GEVulkanTextureDescriptor::getTextureID(const irr::video::ITexture** list)
|
||||
{
|
||||
TextureList key =
|
||||
{{
|
||||
m_white_image,
|
||||
m_transparent_image,
|
||||
m_transparent_image,
|
||||
m_transparent_image,
|
||||
m_transparent_image,
|
||||
m_transparent_image,
|
||||
m_transparent_image,
|
||||
m_transparent_image
|
||||
}};
|
||||
for (unsigned i = 0; i < m_max_layer; i++)
|
||||
{
|
||||
if (list[i])
|
||||
{
|
||||
key[i] = static_cast<const GEVulkanTexture*>(
|
||||
list[i])->getImageView();
|
||||
}
|
||||
}
|
||||
auto it = m_texture_list.find(key);
|
||||
if (it != m_texture_list.end())
|
||||
return it->second;
|
||||
else
|
||||
{
|
||||
int cur_id = m_texture_list.size();
|
||||
if (cur_id >= m_max_texture_list)
|
||||
{
|
||||
printf("Too many texture used in current frames\n");
|
||||
m_recreate_next_frame = true;
|
||||
return m_max_texture_list - 1;
|
||||
}
|
||||
|
||||
m_texture_list[key] = cur_id;
|
||||
m_needs_update_descriptor = true;
|
||||
// Reset the list earlier if almost full
|
||||
if (cur_id > int((float)m_max_texture_list * 0.8f))
|
||||
m_recreate_next_frame = true;
|
||||
return cur_id;
|
||||
}
|
||||
} // getTextureID
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user