Files
watrbx-game-engine/Rendering/GfxRender/TextureCompositor.cpp
T
2025-09-18 17:55:52 -04:00

1056 lines
34 KiB
C++

#include "stdafx.h"
#include "TextureCompositor.h"
#include "GfxBase/AsyncResult.h"
#include "v8datamodel/ContentProvider.h"
#include "v8datamodel/MeshContentProvider.h"
#include "v8datamodel/PartInstance.h"
#include "GfxBase/RenderCaps.h"
#include "GfxBase/RenderStats.h"
#include "VisualEngine.h"
#include "TextureManager.h"
#include "ShaderManager.h"
#include "GlobalShaderData.h"
#include "GfxBase/FileMeshData.h"
#include "GfxCore/Geometry.h"
#include "GfxCore/Texture.h"
#include "GfxCore/Framebuffer.h"
#include "GfxCore/Device.h"
#include "GfxCore/States.h"
#include "rbx/Profiler.h"
LOGVARIABLE(RenderTextureCompositor, 0)
LOGVARIABLE(RenderTextureCompositorBudget, 0)
namespace RBX
{
namespace Graphics
{
#if defined(RBX_PLATFORM_IOS) || defined(__ANDROID__)
static const size_t kTextureCompositorActiveJobs = 2;
#else
static const size_t kTextureCompositorActiveJobs = 8;
#endif
static const size_t kTextureCompositorCooldown = 2;
static const double kRenderTextureCompositorRebakeDelaySeconds = 1.0;
static const unsigned int kTextureCompositorDefaultBudget = 8 * 1024 * 1024;
static const unsigned int kTextureCompositorOrphanedBudgetLimit = 32 * 1024 * 1024;
static const unsigned int kTextureCompositorOrphanedKeepAlive = 2;
#if defined(RBX_PLATFORM_IOS) || defined(__ANDROID__)
// OpenGL blits are really slow on iPad since they go through system memory (~240 ms for 1024x512)
static const bool kTextureCompositorUseRenderTextures = true;
// Conserve memory by using 16-bit textures
static const bool kTextureCompositorUse32BitTextures = false;
#else
static const bool kTextureCompositorUseRenderTextures = false;
static const bool kTextureCompositorUse32BitTextures = true;
#endif
static TextureCompositorConfiguration calculateConfiguration(const RenderCaps* caps)
{
// Work around budget detection issues
// An example of this is that when we run headless (RCCService), we'd like to have some reasonable default for budget size.
// This also applies if the driver misreport VRAM size as zero.
unsigned int budget =
FLog::RenderTextureCompositorBudget
? FLog::RenderTextureCompositorBudget * 1024 * 1024
: std::max(kTextureCompositorDefaultBudget, static_cast<unsigned int>(caps->getVidMemSize() / 3));
TextureCompositorConfiguration config;
config.bpp = kTextureCompositorUse32BitTextures ? 32 : 16;
config.budget = budget;
return config;
}
static unsigned int getTextureSize(const shared_ptr<Texture>& tex)
{
return tex ? Texture::getImageSize(tex->getFormat(), tex->getWidth(), tex->getHeight()) : 0;
}
static unsigned int getTextureSize(unsigned int width, unsigned int height, unsigned int bpp)
{
return width * height * (bpp / 8);
}
template <typename T> struct WeakPtrEqualPredicate
{
const boost::weak_ptr<T>* lhs;
WeakPtrEqualPredicate(const boost::weak_ptr<T>* lhs): lhs(lhs)
{
}
bool operator()(const boost::weak_ptr<T>& rhs) const
{
return !(*lhs < rhs || rhs < *lhs);
}
};
template <typename T> struct ExistsInSetPredicate
{
const std::set<T>* set;
ExistsInSetPredicate(const std::set<T>* set): set(set)
{
}
bool operator()(const T& object) const
{
return set->count(object) > 0;
}
};
template <typename T> struct PriorityComparator
{
bool operator()(const T& lhs, const T& rhs) const
{
return lhs->priority < rhs->priority;
}
};
struct DistanceUpdatePredicate
{
void* job;
Vector3 focus;
float* distance;
DistanceUpdatePredicate(void* job, const Vector3& focus, float* distance): job(job), focus(focus), distance(distance)
{
}
bool operator()(const boost::weak_ptr<PartInstance>& locptr) const
{
boost::shared_ptr<PartInstance> loc = locptr.lock();
if (!loc)
{
FASTLOG1(FLog::RenderTextureCompositor, "TC Job[%p]: detach instance", job);
return true;
}
*distance = std::min(*distance, (focus - loc->getCoordinateFrame().translation).squaredMagnitude());
return false;
}
};
template <typename T> struct TextureIsNullPredicate
{
bool operator()(const T& obj) const
{
return !obj->texture;
}
};
std::string TextureCompositorLayer::toString() const
{
return format("L[%s:%s:%08x:%d]", mesh.c_str(), texture.c_str(), Color4uint8(color).asUInt32(), mode);
}
class TextureCompositorMeshCache
{
public:
TextureCompositorMeshCache(VisualEngine* visualEngine)
: visualEngine(visualEngine)
{
std::vector<VertexLayout::Element> elements;
elements.push_back(VertexLayout::Element(0, offsetof(Vertex, position), VertexLayout::Format_Float3, VertexLayout::Semantic_Position));
elements.push_back(VertexLayout::Element(0, offsetof(Vertex, uv), VertexLayout::Format_Float2, VertexLayout::Semantic_Texture));
layout = visualEngine->getDevice()->createVertexLayout(elements);
}
const shared_ptr<GeometryBatch>* requestMesh(const MeshId& meshId)
{
// Check mesh cache first; it's likely to have the mesh loaded
Meshes::iterator it = meshes.find(meshId);
if (it != meshes.end())
return &it->second;
// Try to fetch the mesh using content provider
MeshContentProvider* mcp = visualEngine->getMeshContentProvider();
AsyncHttpQueue::RequestResult result;
shared_ptr<FileMeshData> cachedMesh = boost::static_pointer_cast<FileMeshData>(mcp->requestContent(meshId, ContentProvider::PRIORITY_MESH, false, result));
if (result == AsyncHttpQueue::Waiting)
return NULL;
shared_ptr<GeometryBatch>& cache = meshes[meshId];
if (result == AsyncHttpQueue::Succeeded)
{
// Fill cache with new mesh
GeometryBatch mesh = createMesh(visualEngine->getDevice(), cachedMesh.get());
cache.reset(new GeometryBatch(mesh));
}
return &cache;
}
private:
struct Vertex
{
Vector3 position;
Vector2 uv;
};
VisualEngine* visualEngine;
shared_ptr<VertexLayout> layout;
typedef boost::unordered_map<MeshId, shared_ptr<GeometryBatch> > Meshes;
Meshes meshes;
GeometryBatch createMesh(Device* device, FileMeshData* data)
{
// Create and fill vertex buffer
shared_ptr<VertexBuffer> vbuf = device->createVertexBuffer(sizeof(Vertex), data->vnts.size(), GeometryBuffer::Usage_Static);
Vertex* vbptr = static_cast<Vertex*>(vbuf->lock());
for (size_t i = 0; i < data->vnts.size(); ++i)
{
const FileMeshVertexNormalTexture3d& v = data->vnts[i];
vbptr[i].position = Vector3(v.vx, v.vy, v.vz);
vbptr[i].uv = Vector2(v.tu, v.tv);
}
vbuf->unlock();
// Create and fill index buffer
shared_ptr<IndexBuffer> ibuf = device->createIndexBuffer(sizeof(unsigned short), data->faces.size() * 3, GeometryBuffer::Usage_Static);
unsigned short* ibptr = static_cast<unsigned short*>(ibuf->lock());
for (size_t i = 0; i < data->faces.size(); ++i)
{
const FileMeshFace& f = data->faces[i];
ibptr[i * 3 + 0] = f.a;
ibptr[i * 3 + 1] = f.b;
ibptr[i * 3 + 2] = f.c;
}
ibuf->unlock();
// Create geometry batch
shared_ptr<Geometry> geometry = device->createGeometry(layout, vbuf, ibuf);
return GeometryBatch(geometry, Geometry::Primitive_Triangles, data->faces.size() * 3, data->vnts.size());
}
};
class TextureCompositorJob
{
public:
TextureCompositorJob(VisualEngine* visualEngine, const Vector2& canvasSize, const std::vector<TextureCompositorLayer>& layers, float contentPriority, const std::string& context)
: visualEngine(visualEngine)
, canvasSize(canvasSize)
, layers(layers.size())
, contentPriority(contentPriority)
, readyCount(0)
{
for (size_t i = 0; i < layers.size(); ++i)
{
LayerData& ld = this->layers[i];
ld.desc = layers[i];
// Fetch texture
if (!ld.desc.texture.isNull())
{
ld.texture = visualEngine->getTextureManager()->load(ld.desc.texture, TextureManager::Fallback_None, context);
}
else
{
ld.texture = visualEngine->getTextureManager()->getFallbackTexture(TextureManager::Fallback_White);
}
}
}
void update(TextureCompositorMeshCache& meshCache)
{
size_t ready = 0;
for (size_t i = 0; i < layers.size(); ++i)
{
LayerData& ld = layers[i];
// Fetch mesh
if (!ld.mesh)
ld.mesh = meshCache.requestMesh(ld.desc.mesh);
// Verify mesh/texture
if (ld.mesh && ld.texture.getStatus() != TextureRef::Status_Waiting)
ready++;
}
readyCount = ready;
}
void render(DeviceContext* context, const shared_ptr<Framebuffer>& framebuffer)
{
RBXASSERT(isReady());
const DeviceCaps& caps = visualEngine->getDevice()->getCaps();
// half-pixel offset for D3D to account for shifted pixel center during rasterization
float offset = caps.needsHalfPixelOffset ? 0.5f : 0;
Matrix4 projection =
Matrix4(
2.f / canvasSize.x, 0.f, 0.f, -1.f - offset * 2.f / static_cast<float>(framebuffer->getWidth()),
0.f, 2.f / canvasSize.y, 0.f, -1.f + offset * 2.f / static_cast<float>(framebuffer->getHeight()),
0.f, 0.f, 0.001f, 0.5f, // shift depth to 0.5 to avoid clipping; don't multiply Z by zero to avoid d3d debug warnings.
0.f, 0.f, 0.f, 1.f);
if (caps.requiresRenderTargetFlipping)
{
projection.setRow(1, -projection.row(1));
}
RenderCamera orthoCamera;
orthoCamera.setViewMatrix(Matrix4::identity());
orthoCamera.setProjectionMatrix(projection);
GlobalShaderData globalData;
globalData.setCamera(orthoCamera);
context->updateGlobalConstants(&globalData, sizeof(globalData));
context->setRasterizerState(caps.requiresRenderTargetFlipping ? RasterizerState::Cull_Front : RasterizerState::Cull_Back);
context->setDepthState(DepthState(DepthState::Function_Always, false));
context->bindFramebuffer(framebuffer.get());
const float clearColor[] = {0.5f, 0.5f, 0.5f, 0.f};
context->clearFramebuffer(DeviceContext::Buffer_Color, clearColor, 1.f, 0);
for (size_t i = 0; i < layers.size(); ++i)
{
LayerData& ld = layers[i];
shared_ptr<ShaderProgram> program = visualEngine->getShaderManager()->getProgramOrFFP("TexCompVS", ld.desc.mode == TextureCompositorLayer::Composit_BlendTexture ? "TexCompPMAFS" : "TexCompFS");
if (program && *ld.mesh && ld.texture.getTexture())
{
context->setBlendState(ld.desc.mode == TextureCompositorLayer::Composit_BlendTexture ? BlendState::Mode_PremultipliedAlphaBlend : BlendState::Mode_None);
context->bindProgram(program.get());
context->bindTexture(0, ld.texture.getTexture().get(), SamplerState(SamplerState::Filter_Linear, SamplerState::Address_Clamp));
float colorData[] = {ld.desc.color.r, ld.desc.color.g, ld.desc.color.b, (ld.desc.mode == TextureCompositorLayer::Composit_BlitTextureAlphaMagnify4x) ? 4.f : 1.f};
context->setConstant(program->getConstantHandle("Color"), colorData, 1);
context->draw(**ld.mesh);
}
}
}
bool isReady() const
{
return readyCount == layers.size();
}
private:
struct LayerData
{
LayerData(): desc(MeshId(), TextureId()), mesh(NULL)
{
}
TextureCompositorLayer desc;
const shared_ptr<GeometryBatch>* mesh;
TextureRef texture;
};
VisualEngine* visualEngine;
Vector2 canvasSize;
std::vector<LayerData> layers;
float contentPriority;
size_t readyCount;
};
TextureCompositor::RenderedJob::RenderedJob()
: cooldown(0)
{
}
TextureCompositor::RenderedJob::RenderedJob(const shared_ptr<Job>& job, const shared_ptr<Framebuffer>& framebuffer, int cooldown)
: job(job)
, framebuffer(framebuffer)
, cooldown(cooldown)
{
}
TextureCompositor::TextureCompositor(VisualEngine* visualEngine)
: Resource(visualEngine->getDevice())
, visualEngine(visualEngine)
, lastActiveTime(Time::now<Time::Fast>())
{
// get configuration based on available VRAM
config = calculateConfiguration(visualEngine->getRenderCaps());
meshCache.reset(new TextureCompositorMeshCache(visualEngine));
}
TextureCompositor::~TextureCompositor()
{
}
TextureCompositor::JobHandle TextureCompositor::getJob(const std::string& textureid, const std::string& context, unsigned int width, unsigned int height, const Vector2& canvasSize, const std::vector<TextureCompositorLayer>& layers)
{
// look for an existing job
boost::shared_ptr<Job>& job = jobs[textureid];
if (job) return job;
// look for an orphaned job with the same id
for (size_t i = 0; i < orphanedJobs.size(); ++i)
{
const boost::shared_ptr<Job>& orphaned = orphanedJobs[i];
if (orphaned->desc.textureid == textureid && orphaned->texture)
{
// we recently orphaned a job with the same description and a texture which is still valid
// we have to readd the job to job list, but we don't have to rebake it - just reuse the contents
FASTLOG1(FLog::RenderTextureCompositor, "TC Job[%p]: restoring orphaned job", orphaned.get());
job = orphaned;
job->textureRef = job->texture;
orphanedJobs.erase(orphanedJobs.begin() + i);
return job;
}
}
// create new job
job.reset(new Job());
job->desc.textureid = textureid;
job->desc.width = width;
job->desc.height = height;
job->desc.canvasSize = canvasSize;
job->desc.layers = layers;
job->context = context;
job->priority = FLT_MAX;
job->textureRef = TextureRef::future(visualEngine->getTextureManager()->getFallbackTexture(TextureManager::Fallback_Gray));
FASTLOG4(FLog::RenderTextureCompositor, "TC Job[%p]: create %dx%d (%d layers)", job.get(), width, height, layers.size());
FASTLOGS(FLog::RenderTextureCompositor, "TC Job texture: %s", textureid.c_str());
for (size_t i = 0; i < layers.size(); ++i)
{
FASTLOGS(FLog::RenderTextureCompositor, "TC Job layer: %s", layers[i].toString().c_str());
}
// queue job for processing on subsequent frames
pendingJobs.push_back(job);
FASTLOG1(FLog::RenderTextureCompositor, "TC Job[%p]: queueing (external request)", job.get());
return job;
}
TextureRef TextureCompositor::getTexture(const JobHandle& job)
{
RBXASSERT(job);
return job->textureRef;
}
const std::string& TextureCompositor::getTextureId(const JobHandle& job)
{
RBXASSERT(job);
return job->desc.textureid;
}
void TextureCompositor::attachInstance(const JobHandle& job, const boost::shared_ptr<PartInstance>& instance)
{
RBXASSERT(job);
if (!instance) return;
boost::weak_ptr<PartInstance> weakptr = instance;
if (std::find_if(job->instances.begin(), job->instances.end(), WeakPtrEqualPredicate<PartInstance>(&weakptr)) == job->instances.end())
{
job->instances.push_back(weakptr);
FASTLOG2(FLog::RenderTextureCompositor, "TC Job[%p]: attach instance %p", job.get(), instance.get());
FASTLOGS(FLog::RenderTextureCompositor, "TC Job instance: %s", instance->getFullName().c_str());
}
}
bool TextureCompositor::isQueueEmpty() const
{
return pendingJobs.empty() && activeJobs.empty() && !renderedJob.job;
}
void TextureCompositor::updatePrioritiesAndOrphanJobs(const Vector3& pointOfInterest)
{
std::set<boost::shared_ptr<Job> > newOrphanedJobs;
// update priorities for all jobs and keep track of jobs we no longer need
for (JobMap::iterator it = jobs.begin(); it != jobs.end(); )
{
Job& job = *it->second;
// update priority and discard dead instances
float distance = FLT_MAX;
job.instances.erase(std::remove_if(job.instances.begin(), job.instances.end(), DistanceUpdatePredicate(&job, pointOfInterest, &distance)), job.instances.end());
job.priority = distance;
// remove jobs where texture is not needed any more (no point regenerating the texture)
if (job.textureRef.isUnique())
{
FASTLOG1(FLog::RenderTextureCompositor, "TC Job[%p]: orphaning job (no materials)", &job);
// cleanup job state
job.textureRef = TextureRef();
job.job.reset();
job.instances.clear();
newOrphanedJobs.insert(it->second);
// erase old job from map, move iterator to the next one
jobs.erase(it++);
}
else
++it;
}
if (!newOrphanedJobs.empty())
{
// add all orphaned jobs to orphaned queue (we'll garbage collect them separately)
orphanedJobs.insert(orphanedJobs.end(), newOrphanedJobs.begin(), newOrphanedJobs.end());
// remove orphaned jobs from all queues
ExistsInSetPredicate<boost::shared_ptr<Job> > isOrphaned(&newOrphanedJobs);
pendingJobs.erase(std::remove_if(pendingJobs.begin(), pendingJobs.end(), isOrphaned), pendingJobs.end());
activeJobs.erase(std::remove_if(activeJobs.begin(), activeJobs.end(), isOrphaned), activeJobs.end());
}
}
void TextureCompositor::garbageCollectOrphanedJobs()
{
unsigned int totalSize = getTotalLiveTextureSize();
unsigned int orphanedSize = getTotalOrphanedTextureSize();
// we always keep N low-res textures so that downsampling or creating a new outfit does not require a texture allocation
// otherwise we cap the total orphaned size by remaining budget, but no more than 25% of the budget and no more than a fixed limit
unsigned int maxOrphanedSize = (totalSize > config.budget) ? 0 : std::min(config.budget - totalSize, std::min(config.budget / 4, kTextureCompositorOrphanedBudgetLimit));
if (orphanedSize > maxOrphanedSize)
{
std::vector<char> sweep(orphanedJobs.size());
// sweep textures starting from the front (textures at the back are LRU) while we exceed the orphaned size budget
for (size_t i = 0; i < orphanedJobs.size(); ++i)
{
if (orphanedSize > maxOrphanedSize)
{
orphanedSize -= getTextureSize(orphanedJobs[i]->texture);
sweep[i] = true;
}
}
// don't sweep last N low-res textures
unsigned int keepAlive = kTextureCompositorOrphanedKeepAlive;
for (size_t i = orphanedJobs.size(); i > 0; --i)
{
const shared_ptr<Job>& job = orphanedJobs[i - 1];
const shared_ptr<Texture>& texture = job->texture;
if (texture && texture->getWidth() < job->desc.width && keepAlive > 0)
{
keepAlive--;
sweep[i - 1] = false;
}
}
// sweep!
for (size_t i = 0; i < sweep.size(); ++i)
{
shared_ptr<Texture>& texture = orphanedJobs[i]->texture;
if (sweep[i] && texture)
{
FASTLOG1(FLog::RenderTextureCompositor, "TC Destroy texture %p", texture.get());
texture.reset();
if (orphanedJobs[i] == renderedJob.job)
{
FASTLOG1(FLog::RenderTextureCompositor, "TC Job[%p]: cancelling blit in progress since the texture is destroyed", orphanedJobs[i].get());
renderedJob = RenderedJob();
}
}
}
}
// destroy all orphaned jobs that did not have a texture or don't have a texture after cleanup above
orphanedJobs.erase(std::remove_if(orphanedJobs.begin(), orphanedJobs.end(), TextureIsNullPredicate<boost::shared_ptr<Job> >()), orphanedJobs.end());
}
void TextureCompositor::findRebakeTargetAndEnqueue()
{
unsigned int totalSize = getTotalLiveTextureSize();
// gather all jobs and sort by priority
std::vector<boost::shared_ptr<Job> > sortedJobs;
sortedJobs.reserve(jobs.size());
for (JobMap::const_iterator it = jobs.begin(); it != jobs.end(); ++it)
sortedJobs.push_back(it->second);
std::sort(sortedJobs.begin(), sortedJobs.end(), PriorityComparator<boost::shared_ptr<Job> >());
// get indices for left-most low-res and right-most high-res texture
size_t leftMostLow = sortedJobs.size();
size_t rightMostHighPlus1 = 0;
for (size_t i = 0; i < sortedJobs.size(); ++i)
{
const shared_ptr<Job>& job = sortedJobs[i];
const shared_ptr<Texture>& texture = job->texture;
if (texture)
{
if (texture->getWidth() < job->desc.width)
leftMostLow = std::min(leftMostLow, i);
else
rightMostHighPlus1 = std::max(rightMostHighPlus1, i + 1);
}
}
// we can upsample something
if (leftMostLow < sortedJobs.size())
{
const shared_ptr<Job>& job = sortedJobs[leftMostLow];
if (totalSize + getTextureSize(job->desc.width, job->desc.height, config.bpp) < config.budget)
{
FASTLOG1(FLog::RenderTextureCompositor, "TC Job[%p]: queueing (upsample)", job.get());
pendingJobs.push_back(job);
return;
}
}
// we can downsample something
if (rightMostHighPlus1 > 0)
{
size_t rightMostHigh = rightMostHighPlus1 - 1;
// equilibrium conditions:
// - all high-res textures have priority less than all low-res textures
// - we are below texture budget
if (rightMostHigh < leftMostLow && totalSize < config.budget)
;
else
{
FASTLOG1(FLog::RenderTextureCompositor, "TC Job[%p]: queueing (downsample)", sortedJobs[rightMostHigh].get());
pendingJobs.push_back(sortedJobs[rightMostHigh]);
}
}
}
void TextureCompositor::garbageCollectFull()
{
updatePrioritiesAndOrphanJobs(Vector3());
for (size_t i = 0; i < orphanedJobs.size(); ++i)
{
shared_ptr<Texture>& texture = orphanedJobs[i]->texture;
if (texture)
{
FASTLOG1(FLog::RenderTextureCompositor, "TC Destroy texture %p", texture.get());
texture.reset();
if (orphanedJobs[i] == renderedJob.job)
{
FASTLOG1(FLog::RenderTextureCompositor, "TC Job[%p]: cancelling blit in progress since the texture is destroyed", orphanedJobs[i].get());
renderedJob = RenderedJob();
}
}
}
orphanedJobs.clear();
}
void TextureCompositor::cancelPendingRequests()
{
jobs.clear();
pendingJobs.clear();
activeJobs.clear();
orphanedJobs.clear();
renderedJob = RenderedJob();
}
void TextureCompositor::update(const Vector3& pointOfInterest)
{
// this gets rid of all jobs that we don't need to process anyway
updatePrioritiesAndOrphanJobs(pointOfInterest);
// orphaned queue serves as a cache and a texture pool at the same time; make sure it does not grow too big
garbageCollectOrphanedJobs();
// if we have nothing else to do, let's try to find some textures that need to have a different resolution
if (isQueueEmpty())
{
double latency = (Time::now<Time::Fast>() - lastActiveTime).seconds();
if (latency >= kRenderTextureCompositorRebakeDelaySeconds)
{
findRebakeTargetAndEnqueue();
}
}
else
{
lastActiveTime = Time::now<Time::Fast>();
}
// move some jobs from pending to active
if (pendingJobs.size() > 0 && activeJobs.size() < kTextureCompositorActiveJobs)
{
size_t count = std::min(pendingJobs.size(), kTextureCompositorActiveJobs - activeJobs.size());
std::nth_element(pendingJobs.begin(), pendingJobs.begin() + count, pendingJobs.end(), PriorityComparator<boost::shared_ptr<Job> >());
activeJobs.insert(activeJobs.end(), pendingJobs.begin(), pendingJobs.begin() + count);
pendingJobs.erase(pendingJobs.begin(), pendingJobs.begin() + count);
}
// update active job order (important for renderJobIfNecessary)
std::sort(activeJobs.begin(), activeJobs.end(), PriorityComparator<boost::shared_ptr<Job> >());
// update active jobs
for (size_t i = 0; i < activeJobs.size(); ++i)
{
updateJob(*activeJobs[i]);
}
}
void TextureCompositor::render(DeviceContext* context)
{
RBXPROFILER_SCOPE("Render", "TextureCompositor::render");
RBXPROFILER_SCOPE("GPU", "TextureCompositor::render");
if (renderedJob.job)
{
// finalize the job that we rendered before
--renderedJob.cooldown;
if (renderedJob.cooldown <= 0)
{
renderJobFinalize(*renderedJob.job, renderedJob.framebuffer, context);
renderedJob = RenderedJob();
}
}
else
{
// render at most one active job
for (size_t i = 0; i < activeJobs.size(); ++i)
{
Job& job = *activeJobs[i];
if (job.job && job.job->isReady())
{
// render and update materials to use new texture
renderJobIfNecessary(job, i, context);
// make sure we don't keep render data alive
job.job.reset();
// remove job from active queue
activeJobs.erase(activeJobs.begin() + i);
break;
}
}
}
}
void TextureCompositor::updateJob(Job& job)
{
if (!job.job)
{
FASTLOG2(FLog::RenderTextureCompositor, "TC Job[%p]: start loading assets (priority %d)", &job, (int)sqrtf(job.priority));
// convert from [0..+inf) to [0..1] while keeping the ordering; distribution is not very important
float priority = 1.f - 1.f / (1.f + sqrtf(job.priority));
job.job.reset(new TextureCompositorJob(visualEngine, job.desc.canvasSize, job.desc.layers, ContentProvider::PRIORITY_CHARACTER + priority, job.context));
}
job.job->update(*meshCache);
}
void TextureCompositor::renderJobFinalize(Job& job, const shared_ptr<Framebuffer>& framebuffer, DeviceContext* context)
{
const shared_ptr<Texture>& texture = job.texture;
RBXASSERT(texture);
if (texture->getUsage() != Texture::Usage_Renderbuffer)
{
try
{
Timer<Time::Precise> timer;
context->copyFramebuffer(framebuffer.get(), texture.get());
FASTLOG3(FLog::RenderTextureCompositor, "TC Job[%p]: texture blit (width %d) took %d us", &job, texture->getWidth(), (int)(timer.delta().msec() * 1000));
}
catch (const RBX::base_exception& e)
{
FASTLOG2(FLog::RenderTextureCompositor, "TC Job[%p]: texture blit (width %d) failed", &job, texture->getWidth());
FASTLOGS(FLog::RenderTextureCompositor, "TC: Failure reason %s", e.what());
RBX::StandardOut::singleton()->printf(MESSAGE_OUTPUT,"TextureCompositor copyFramebuffer failed: %s", e.what());
}
}
// update texture reference
if (job.textureRef.getStatus() != TextureRef::Status_Null)
job.textureRef.updateAllRefs(texture);
}
void TextureCompositor::renderJobIfNecessary(Job& job, size_t activePosition, DeviceContext* context)
{
unsigned int totalSize = getTotalLiveTextureSize();
// assuming that all other textures in the queue are new (they should be, since we don't rebake with non-empty queue),
// we can estimate a final texture size better if we assume that we already created all other textures with suitable quality
unsigned int pendingSize =
getProjectedPendingTextureSize() / 4 + // all pending jobs are assumed to get low-res texture
getProjectedActiveTextureSize(activePosition + 1, activeJobs.size()) / 4 + // all active jobs after us are assumed to get low-res texture
getProjectedActiveTextureSize(0, activePosition); // all active jobs before us are assumed to get high-res textures (even if our assets load first we don't get high-res texture out of order)
// create a high-quality texture only if we're within budget after we create the texture
unsigned int textureSize = getTextureSize(job.desc.width, job.desc.height, config.bpp);
bool hq = (totalSize + pendingSize + textureSize < config.budget);
unsigned int width = hq ? job.desc.width : job.desc.width / 2;
unsigned int height = hq ? job.desc.height : job.desc.height / 2;
if (!job.texture || job.texture->getWidth() != width || job.texture->getHeight() != height)
{
if (job.texture)
{
// We're resampling an existing job; rather than lose the texture, let's add it to orphaned queue as an empty job - garbage collection will take care of it
orphanTextureFromJob(job);
}
FASTLOG5(FLog::RenderTextureCompositor, "TC Job[%p]: render (totalSize %d, pendingSize %d, budget %d -> width %d)", &job, totalSize, pendingSize, config.budget, width);
shared_ptr<Texture> texture = getOrCreateTexture(width, height);
shared_ptr<Framebuffer> framebuffer = getOrCreateFramebufer(texture);
job.job->render(context, framebuffer);
// make sure the job uses the texture so that we can finalize it later at some point
job.texture = texture;
// queue job for some final processing
RBXASSERT(activeJobs[activePosition].get() == &job);
renderedJob = RenderedJob(activeJobs[activePosition], framebuffer, kTextureCompositorCooldown);
}
}
void TextureCompositor::orphanTextureFromJob(Job& job)
{
boost::shared_ptr<Job> textureJob(new Job());
textureJob->priority = FLT_MAX;
textureJob->texture = job.texture;
FASTLOG2(FLog::RenderTextureCompositor, "TC Job[%p]: store previous texture as orphaned job %p", &job, textureJob.get());
orphanedJobs.push_back(textureJob);
job.texture.reset();
}
unsigned int TextureCompositor::getTotalLiveTextureSize()
{
unsigned int result = 0;
for (JobMap::const_iterator it = jobs.begin(); it != jobs.end(); ++it)
result += getTextureSize(it->second->texture);
return result;
}
unsigned int TextureCompositor::getTotalOrphanedTextureSize()
{
unsigned int result = 0;
for (size_t i = 0; i < orphanedJobs.size(); ++i)
result += getTextureSize(orphanedJobs[i]->texture);
return result;
}
unsigned int TextureCompositor::getProjectedPendingTextureSize()
{
unsigned int result = 0;
for (size_t i = 0; i < pendingJobs.size(); ++i)
{
const shared_ptr<Job>& job = pendingJobs[i];
result += getTextureSize(job->desc.width, job->desc.height, config.bpp);
}
return result;
}
unsigned int TextureCompositor::getProjectedActiveTextureSize(size_t begin, size_t end)
{
unsigned int result = 0;
for (size_t i = begin; i < end; ++i)
{
const shared_ptr<Job>& job = activeJobs[i];
result += getTextureSize(job->desc.width, job->desc.height, config.bpp);
}
return result;
}
shared_ptr<Framebuffer> TextureCompositor::getOrCreateFramebufer(const shared_ptr<Texture>& texture)
{
// we can render into the texture if it's a render target itself
if (texture->getUsage() == Texture::Usage_Renderbuffer)
return visualEngine->getDevice()->createFramebuffer(texture->getRenderbuffer(0, 0));
// look for matching framebuffer in cache
for (size_t i = 0; i < framebuffers.size(); ++i)
{
if (framebuffers[i]->getWidth() == texture->getWidth() && framebuffers[i]->getHeight() == texture->getHeight())
return framebuffers[i];
}
// create a new framebuffer
shared_ptr<Texture> rt = visualEngine->getDevice()->createTexture(Texture::Type_2D, Texture::Format_RGBA8, texture->getWidth(), texture->getHeight(), 1, 1, Texture::Usage_Renderbuffer);
shared_ptr<Framebuffer> framebuffer = visualEngine->getDevice()->createFramebuffer(rt->getRenderbuffer(0, 0));
// we make sure RTs are always alive to minimize stalls (there should be <3 Mb of them anyway)
framebuffers.push_back(framebuffer);
return framebuffer;
}
shared_ptr<Texture> TextureCompositor::getOrCreateTexture(unsigned int width, unsigned int height)
{
// try to steal a texture from orphaned queue
for (size_t i = 0; i < orphanedJobs.size(); ++i)
{
shared_ptr<Texture> texture = orphanedJobs[i]->texture;
if (texture && texture->getWidth() == width && texture->getHeight() == height)
{
FASTLOG1(FLog::RenderTextureCompositor, "TC Reuse texture %p", texture.get());
orphanedJobs.erase(orphanedJobs.begin() + i);
return texture;
}
}
Texture::Format format = config.bpp == 16 ? Texture::Format_RGB5A1 : Texture::Format_RGBA8;
Texture::Usage usage = kTextureCompositorUseRenderTextures ? Texture::Usage_Renderbuffer : Texture::Usage_Static;
shared_ptr<Texture> texture = visualEngine->getDevice()->createTexture(Texture::Type_2D, format, width, height, 1, 1, usage);
FASTLOG1(FLog::RenderTextureCompositor, "TC Create texture %p", texture.get());
return texture;
}
TextureCompositorStats TextureCompositor::getStatistics() const
{
TextureCompositorStats result = {};
for (JobMap::const_iterator it = jobs.begin(); it != jobs.end(); ++it)
{
const shared_ptr<Job>& job = it->second;
const shared_ptr<Texture>& texture = job->texture;
if (!texture)
continue;
if (texture->getWidth() == job->desc.width)
{
result.liveHQCount++;
result.liveHQSize += getTextureSize(texture);
}
else
{
result.liveLQCount++;
result.liveLQSize += getTextureSize(texture);
}
}
for (size_t i = 0; i < orphanedJobs.size(); ++i)
{
result.orphanedCount++;
result.orphanedSize += getTextureSize(orphanedJobs[i]->texture);
}
return result;
}
void TextureCompositor::onDeviceLost()
{
FASTLOG(FLog::RenderTextureCompositor, "TC Device lost");
if (shared_ptr<Job> job = renderedJob.job)
{
// We're going to blit the texture at some point in the future; however, we've just lost the texture contents.
// Let's put the job back to the pending queue.
FASTLOG1(FLog::RenderTextureCompositor, "TC Job[%p]: device lost while job render is in progress, enqueue job once again", job.get());
orphanTextureFromJob(*job);
pendingJobs.push_back(job);
renderedJob = RenderedJob();
}
}
}
}