mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-26 02:15:08 +02:00
Remove duplicate code, polish
This commit is contained in:
parent
0d89b3a922
commit
283ff01038
4 changed files with 82 additions and 162 deletions
|
@ -69,7 +69,7 @@ GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texturePointer) {
|
||||||
|
|
||||||
GL41Texture::GL41Texture(const std::weak_ptr<GLBackend>& backend, const Texture& texture)
|
GL41Texture::GL41Texture(const std::weak_ptr<GLBackend>& backend, const Texture& texture)
|
||||||
: GLTexture(backend, texture, allocate()), _storageStamp { texture.getStamp() }, _size(texture.evalTotalSize()) {
|
: GLTexture(backend, texture, allocate()), _storageStamp { texture.getStamp() }, _size(texture.evalTotalSize()) {
|
||||||
|
incrementTextureGPUCount();
|
||||||
withPreservedTexture([&] {
|
withPreservedTexture([&] {
|
||||||
GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat());
|
GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat());
|
||||||
const Sampler& sampler = _gpuObject.getSampler();
|
const Sampler& sampler = _gpuObject.getSampler();
|
||||||
|
|
|
@ -94,15 +94,27 @@ public:
|
||||||
Undersubscribed,
|
Undersubscribed,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
using QueuePair = std::pair<TextureWeakPointer, float>;
|
||||||
|
class QueuePairLess {
|
||||||
|
public:
|
||||||
|
bool operator()(const QueuePair& a, const QueuePair& b) {
|
||||||
|
return a.second < b.second;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
using WorkQueue = std::priority_queue<QueuePair, std::vector<QueuePair>, QueuePairLess>;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
static std::atomic<bool> _memoryPressureStateStale;
|
static std::atomic<bool> _memoryPressureStateStale;
|
||||||
static MemoryPressureState _memoryPressureState;
|
static MemoryPressureState _memoryPressureState;
|
||||||
static std::list<TextureWeakPointer> _memoryManagedTextures;
|
static std::list<TextureWeakPointer> _memoryManagedTextures;
|
||||||
|
static WorkQueue _workQueue;
|
||||||
static const uvec3 INITIAL_MIP_TRANSFER_DIMENSIONS;
|
static const uvec3 INITIAL_MIP_TRANSFER_DIMENSIONS;
|
||||||
|
|
||||||
|
|
||||||
static void updateMemoryPressure();
|
static void updateMemoryPressure();
|
||||||
static void processWorkQueues();
|
static void processWorkQueues();
|
||||||
static void addMemoryManagedTexture(const TexturePointer& texturePointer);
|
static void addMemoryManagedTexture(const TexturePointer& texturePointer);
|
||||||
|
static void addToWorkQueue(const TexturePointer& texture);
|
||||||
|
|
||||||
static void manageMemory();
|
static void manageMemory();
|
||||||
|
|
||||||
|
|
|
@ -107,6 +107,7 @@ using GL45Texture = GL45Backend::GL45Texture;
|
||||||
|
|
||||||
GL45Texture::GL45Texture(const std::weak_ptr<GLBackend>& backend, const Texture& texture)
|
GL45Texture::GL45Texture(const std::weak_ptr<GLBackend>& backend, const Texture& texture)
|
||||||
: GLTexture(backend, texture, allocate(texture)) {
|
: GLTexture(backend, texture, allocate(texture)) {
|
||||||
|
incrementTextureGPUCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
GLuint GL45Texture::allocate(const Texture& texture) {
|
GLuint GL45Texture::allocate(const Texture& texture) {
|
||||||
|
|
|
@ -30,11 +30,13 @@ using namespace gpu::gl45;
|
||||||
// Variable sized textures
|
// Variable sized textures
|
||||||
using GL45VariableAllocationTexture = GL45Backend::GL45VariableAllocationTexture;
|
using GL45VariableAllocationTexture = GL45Backend::GL45VariableAllocationTexture;
|
||||||
using MemoryPressureState = GL45VariableAllocationTexture::MemoryPressureState;
|
using MemoryPressureState = GL45VariableAllocationTexture::MemoryPressureState;
|
||||||
|
using WorkQueue = GL45VariableAllocationTexture::WorkQueue;
|
||||||
|
|
||||||
std::list<TextureWeakPointer> GL45VariableAllocationTexture::_memoryManagedTextures;
|
std::list<TextureWeakPointer> GL45VariableAllocationTexture::_memoryManagedTextures;
|
||||||
MemoryPressureState GL45VariableAllocationTexture::_memoryPressureState = MemoryPressureState::Idle;
|
MemoryPressureState GL45VariableAllocationTexture::_memoryPressureState = MemoryPressureState::Idle;
|
||||||
std::atomic<bool> GL45VariableAllocationTexture::_memoryPressureStateStale { false };
|
std::atomic<bool> GL45VariableAllocationTexture::_memoryPressureStateStale { false };
|
||||||
const uvec3 GL45VariableAllocationTexture::INITIAL_MIP_TRANSFER_DIMENSIONS { 64, 64, 1 };
|
const uvec3 GL45VariableAllocationTexture::INITIAL_MIP_TRANSFER_DIMENSIONS { 64, 64, 1 };
|
||||||
|
WorkQueue GL45VariableAllocationTexture::_workQueue;
|
||||||
|
|
||||||
#define OVERSUBSCRIBED_PRESSURE_VALUE 0.95f
|
#define OVERSUBSCRIBED_PRESSURE_VALUE 0.95f
|
||||||
#define UNDERSUBSCRIBED_PRESSURE_VALUE 0.85f
|
#define UNDERSUBSCRIBED_PRESSURE_VALUE 0.85f
|
||||||
|
@ -42,45 +44,29 @@ const uvec3 GL45VariableAllocationTexture::INITIAL_MIP_TRANSFER_DIMENSIONS { 64,
|
||||||
|
|
||||||
static const size_t DEFAULT_ALLOWED_TEXTURE_MEMORY = MB_TO_BYTES(DEFAULT_ALLOWED_TEXTURE_MEMORY_MB);
|
static const size_t DEFAULT_ALLOWED_TEXTURE_MEMORY = MB_TO_BYTES(DEFAULT_ALLOWED_TEXTURE_MEMORY_MB);
|
||||||
|
|
||||||
using QueuePair = std::pair<TextureWeakPointer, uint32_t>;
|
|
||||||
class QueuePairLess {
|
|
||||||
public:
|
|
||||||
bool operator()(const QueuePair& a, const QueuePair& b) {
|
|
||||||
return a.second < b.second;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
class QueuePairGreater {
|
|
||||||
public:
|
|
||||||
bool operator()(const QueuePair& a, const QueuePair& b) {
|
|
||||||
return a.second > b.second;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
using DemoteQueue = std::priority_queue<QueuePair, std::vector<QueuePair>, QueuePairLess>;
|
|
||||||
using PromoteQueue = std::priority_queue<QueuePair, std::vector<QueuePair>, QueuePairGreater>;
|
|
||||||
using TransferQueue = std::queue<TextureWeakPointer>;
|
|
||||||
static DemoteQueue demoteQueue;
|
|
||||||
static PromoteQueue promoteQueue;
|
|
||||||
static TransferQueue transferQueue;
|
|
||||||
|
|
||||||
void GL45VariableAllocationTexture::addMemoryManagedTexture(const TexturePointer& texturePointer) {
|
void GL45VariableAllocationTexture::addMemoryManagedTexture(const TexturePointer& texturePointer) {
|
||||||
_memoryManagedTextures.push_back(texturePointer);
|
_memoryManagedTextures.push_back(texturePointer);
|
||||||
|
addToWorkQueue(texturePointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GL45VariableAllocationTexture::addToWorkQueue(const TexturePointer& texturePointer) {
|
||||||
GL45VariableAllocationTexture* object = Backend::getGPUObject<GL45VariableAllocationTexture>(*texturePointer);
|
GL45VariableAllocationTexture* object = Backend::getGPUObject<GL45VariableAllocationTexture>(*texturePointer);
|
||||||
switch (_memoryPressureState) {
|
switch (_memoryPressureState) {
|
||||||
case MemoryPressureState::Oversubscribed:
|
case MemoryPressureState::Oversubscribed:
|
||||||
if (object->canDemote()) {
|
if (object->canDemote()) {
|
||||||
demoteQueue.push({ texturePointer, object->size() });
|
_workQueue.push({ texturePointer, (float) object->size() });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MemoryPressureState::Undersubscribed:
|
case MemoryPressureState::Undersubscribed:
|
||||||
if (object->canPromote()) {
|
if (object->canPromote()) {
|
||||||
promoteQueue.push({ texturePointer, object->size() });
|
_workQueue.push({ texturePointer, 1.0f / (float)object->size() });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MemoryPressureState::Transfer:
|
case MemoryPressureState::Transfer:
|
||||||
if (object->hasPendingTransfers()) {
|
if (object->hasPendingTransfers()) {
|
||||||
transferQueue.push( texturePointer );
|
_workQueue.push({ texturePointer, 1.0f / (float)object->_gpuObject.evalMipSize(object->_populatedMip) });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -94,23 +80,32 @@ void GL45VariableAllocationTexture::addMemoryManagedTexture(const TexturePointer
|
||||||
|
|
||||||
void GL45VariableAllocationTexture::updateMemoryPressure() {
|
void GL45VariableAllocationTexture::updateMemoryPressure() {
|
||||||
static size_t lastAllowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage();
|
static size_t lastAllowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage();
|
||||||
|
|
||||||
size_t allowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage();
|
size_t allowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage();
|
||||||
|
if (0 == allowedMemoryAllocation) {
|
||||||
|
allowedMemoryAllocation = DEFAULT_ALLOWED_TEXTURE_MEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user explicitly changed the allowed memory usage, we need to mark ourselves stale
|
||||||
|
// so that we react
|
||||||
if (allowedMemoryAllocation != lastAllowedMemoryAllocation) {
|
if (allowedMemoryAllocation != lastAllowedMemoryAllocation) {
|
||||||
_memoryPressureStateStale = true;
|
_memoryPressureStateStale = true;
|
||||||
lastAllowedMemoryAllocation = allowedMemoryAllocation;
|
lastAllowedMemoryAllocation = allowedMemoryAllocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_memoryPressureStateStale) {
|
if (!_memoryPressureStateStale.exchange(false)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_memoryPressureStateStale = false;
|
|
||||||
// Clear any defunct textures
|
// Clear any defunct textures (weak pointers that no longer have a valid texture)
|
||||||
_memoryManagedTextures.remove_if([&](const TextureWeakPointer& weakPointer) {
|
_memoryManagedTextures.remove_if([&](const TextureWeakPointer& weakPointer) {
|
||||||
return weakPointer.expired();
|
return weakPointer.expired();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convert weak pointers to strong
|
// Convert weak pointers to strong. This new list may still contain nulls if a texture was
|
||||||
std::list<TexturePointer> strongTextures; {
|
// deleted on another thread between the previous line and this one
|
||||||
|
std::vector<TexturePointer> strongTextures; {
|
||||||
|
strongTextures.reserve(_memoryManagedTextures.size());
|
||||||
std::transform(
|
std::transform(
|
||||||
_memoryManagedTextures.begin(), _memoryManagedTextures.end(),
|
_memoryManagedTextures.begin(), _memoryManagedTextures.end(),
|
||||||
std::back_inserter(strongTextures),
|
std::back_inserter(strongTextures),
|
||||||
|
@ -127,8 +122,10 @@ void GL45VariableAllocationTexture::updateMemoryPressure() {
|
||||||
if (!texture) {
|
if (!texture) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
idealMemoryAllocation += texture->evalTotalSize();
|
|
||||||
GL45VariableAllocationTexture* object = Backend::getGPUObject<GL45VariableAllocationTexture>(*texture);
|
GL45VariableAllocationTexture* object = Backend::getGPUObject<GL45VariableAllocationTexture>(*texture);
|
||||||
|
// Track how much the texture thinks it should be using
|
||||||
|
idealMemoryAllocation += texture->evalTotalSize();
|
||||||
|
// Track how much we're actually using
|
||||||
totalVariableMemoryAllocation += object->size();
|
totalVariableMemoryAllocation += object->size();
|
||||||
canDemote |= object->canDemote();
|
canDemote |= object->canDemote();
|
||||||
canPromote |= object->canPromote();
|
canPromote |= object->canPromote();
|
||||||
|
@ -136,11 +133,6 @@ void GL45VariableAllocationTexture::updateMemoryPressure() {
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t unallocated = idealMemoryAllocation - totalVariableMemoryAllocation;
|
size_t unallocated = idealMemoryAllocation - totalVariableMemoryAllocation;
|
||||||
if (0 == allowedMemoryAllocation) {
|
|
||||||
allowedMemoryAllocation = DEFAULT_ALLOWED_TEXTURE_MEMORY;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
float pressure = (float)totalVariableMemoryAllocation / (float)allowedMemoryAllocation;
|
float pressure = (float)totalVariableMemoryAllocation / (float)allowedMemoryAllocation;
|
||||||
|
|
||||||
auto newState = MemoryPressureState::Idle;
|
auto newState = MemoryPressureState::Idle;
|
||||||
|
@ -154,142 +146,59 @@ void GL45VariableAllocationTexture::updateMemoryPressure() {
|
||||||
|
|
||||||
if (newState != _memoryPressureState) {
|
if (newState != _memoryPressureState) {
|
||||||
_memoryPressureState = newState;
|
_memoryPressureState = newState;
|
||||||
|
// Clear the existing queue
|
||||||
demoteQueue = DemoteQueue();
|
_workQueue = WorkQueue();
|
||||||
promoteQueue = PromoteQueue();
|
// Populate the existing textures into the queue
|
||||||
transferQueue = TransferQueue();
|
for (const auto& texture : strongTextures) {
|
||||||
|
addToWorkQueue(texture);
|
||||||
switch (_memoryPressureState) {
|
|
||||||
case MemoryPressureState::Idle:
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MemoryPressureState::Oversubscribed:
|
|
||||||
for (const auto& texture : strongTextures) {
|
|
||||||
if (!texture) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
GL45VariableAllocationTexture* object = Backend::getGPUObject<GL45VariableAllocationTexture>(*texture);
|
|
||||||
if (object->canDemote()) {
|
|
||||||
demoteQueue.push({ texture, object->size() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MemoryPressureState::Undersubscribed:
|
|
||||||
for (const auto& texture : strongTextures) {
|
|
||||||
if (!texture) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
GL45VariableAllocationTexture* object = Backend::getGPUObject<GL45VariableAllocationTexture>(*texture);
|
|
||||||
if (object->canPromote()) {
|
|
||||||
promoteQueue.push({ texture, object->size() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MemoryPressureState::Transfer:
|
|
||||||
for (const auto& texture : strongTextures) {
|
|
||||||
if (!texture) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
GL45VariableAllocationTexture* object = Backend::getGPUObject<GL45VariableAllocationTexture>(*texture);
|
|
||||||
if (object->hasPendingTransfers()) {
|
|
||||||
transferQueue.push(texture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
Q_UNREACHABLE();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GL45VariableAllocationTexture::processWorkQueues() {
|
void GL45VariableAllocationTexture::processWorkQueues() {
|
||||||
switch (_memoryPressureState) {
|
if (MemoryPressureState::Idle == _memoryPressureState) {
|
||||||
case MemoryPressureState::Idle:
|
return;
|
||||||
break;
|
}
|
||||||
|
|
||||||
case MemoryPressureState::Oversubscribed:
|
while (!_workQueue.empty()) {
|
||||||
// Grab the first item off the demote queue
|
auto workTarget = _workQueue.top();
|
||||||
while (!demoteQueue.empty()) {
|
_workQueue.pop();
|
||||||
auto demoteTarget = demoteQueue.top();
|
auto texture = workTarget.first.lock();
|
||||||
demoteQueue.pop();
|
if (!texture) {
|
||||||
auto texture = demoteTarget.first.lock();
|
continue;
|
||||||
if (!texture) {
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
GL45VariableAllocationTexture* object = Backend::getGPUObject<GL45VariableAllocationTexture>(*texture);
|
// Grab the first item off the demote queue
|
||||||
if (!object->canDemote()) {
|
GL45VariableAllocationTexture* object = Backend::getGPUObject<GL45VariableAllocationTexture>(*texture);
|
||||||
continue;
|
if (MemoryPressureState::Oversubscribed == _memoryPressureState) {
|
||||||
}
|
if (!object->canDemote()) {
|
||||||
|
continue;
|
||||||
//qDebug() << "QQQ executing demote for " << texture->source().c_str();
|
|
||||||
object->demote();
|
|
||||||
// if the object can be further demoted, reinsert into the queue
|
|
||||||
if (object->canDemote()) {
|
|
||||||
demoteQueue.push({ demoteTarget.first, object->size() });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
if (demoteQueue.empty()) {
|
//qDebug() << "QQQ executing demote for " << texture->source().c_str();
|
||||||
_memoryPressureState = MemoryPressureState::Idle;
|
object->demote();
|
||||||
|
} else if (MemoryPressureState::Undersubscribed == _memoryPressureState) {
|
||||||
|
if (!object->canPromote()) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
break;
|
//qDebug() << "QQQ executing promote for " << texture->source().c_str();
|
||||||
|
object->promote();
|
||||||
case MemoryPressureState::Undersubscribed:
|
} else if (MemoryPressureState::Transfer == _memoryPressureState) {
|
||||||
while (!promoteQueue.empty()) {
|
if (!object->hasPendingTransfers()) {
|
||||||
auto promoteTarget = promoteQueue.top();
|
continue;
|
||||||
promoteQueue.pop();
|
|
||||||
auto texture = promoteTarget.first.lock();
|
|
||||||
if (!texture) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
GL45VariableAllocationTexture* object = Backend::getGPUObject<GL45VariableAllocationTexture>(*texture);
|
|
||||||
if (!object->canPromote()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
//qDebug() << "QQQ executing promote for " << texture->source().c_str();
|
|
||||||
object->promote();
|
|
||||||
if (object->canPromote()) {
|
|
||||||
promoteQueue.push({ promoteTarget.first, object->size() });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
if (promoteQueue.empty()) {
|
//qDebug() << "QQQ executing transfer for " << texture->source().c_str();
|
||||||
_memoryPressureState = MemoryPressureState::Idle;
|
object->executeNextTransfer();
|
||||||
}
|
} else {
|
||||||
break;
|
|
||||||
|
|
||||||
case MemoryPressureState::Transfer:
|
|
||||||
while (!transferQueue.empty()) {
|
|
||||||
auto weakTexture = transferQueue.front();
|
|
||||||
transferQueue.pop();
|
|
||||||
auto texture = weakTexture.lock();
|
|
||||||
if (!texture) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
GL45VariableAllocationTexture* object = Backend::getGPUObject<GL45VariableAllocationTexture>(*texture);
|
|
||||||
if (!object->hasPendingTransfers()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
//qDebug() << "QQQ executing transfer for " << texture->source().c_str();
|
|
||||||
object->executeNextTransfer();
|
|
||||||
if (object->hasPendingTransfers()) {
|
|
||||||
transferQueue.push(weakTexture);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (transferQueue.empty()) {
|
|
||||||
_memoryPressureState = MemoryPressureState::Idle;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
Q_UNREACHABLE();
|
Q_UNREACHABLE();
|
||||||
break;
|
}
|
||||||
|
|
||||||
|
// Reinject into the queue if more work to be done
|
||||||
|
addToWorkQueue(texture);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_workQueue.empty()) {
|
||||||
|
_memoryPressureState = MemoryPressureState::Idle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -379,7 +288,6 @@ void GL45ResourceTexture::promote() {
|
||||||
uint32_t oldSize = _size;
|
uint32_t oldSize = _size;
|
||||||
// create new texture
|
// create new texture
|
||||||
const_cast<GLuint&>(_id) = allocate(_gpuObject);
|
const_cast<GLuint&>(_id) = allocate(_gpuObject);
|
||||||
incrementTextureGPUCount();
|
|
||||||
uint16_t oldAllocatedMip = _allocatedMip;
|
uint16_t oldAllocatedMip = _allocatedMip;
|
||||||
// allocate storage for new level
|
// allocate storage for new level
|
||||||
allocateStorage(_allocatedMip - std::min<uint16_t>(_allocatedMip, 2));
|
allocateStorage(_allocatedMip - std::min<uint16_t>(_allocatedMip, 2));
|
||||||
|
@ -413,7 +321,6 @@ void GL45ResourceTexture::demote() {
|
||||||
auto oldId = _id;
|
auto oldId = _id;
|
||||||
auto oldSize = _size;
|
auto oldSize = _size;
|
||||||
const_cast<GLuint&>(_id) = allocate(_gpuObject);
|
const_cast<GLuint&>(_id) = allocate(_gpuObject);
|
||||||
incrementTextureGPUCount();
|
|
||||||
allocateStorage(_allocatedMip + 1);
|
allocateStorage(_allocatedMip + 1);
|
||||||
_populatedMip = std::max(_populatedMip, _allocatedMip);
|
_populatedMip = std::max(_populatedMip, _allocatedMip);
|
||||||
uint16_t mips = _gpuObject.evalNumMips();
|
uint16_t mips = _gpuObject.evalNumMips();
|
||||||
|
|
Loading…
Reference in a new issue