Add async texture loading

This commit is contained in:
Magnus Norddahl 2025-04-12 01:52:17 +02:00
commit dd782038e4
6 changed files with 225 additions and 4 deletions

View file

@ -37,6 +37,8 @@
#include "vulkan/shaders/vk_shader.h"
#include "vk_hwtexture.h"
CVAR(Bool, gl_async_textures, false, 0);
VkHardwareTexture::VkHardwareTexture(VulkanRenderDevice* fb, int numchannels) : fb(fb)
{
mTexelsize = numchannels;
@ -107,9 +109,39 @@ void VkHardwareTexture::CreateImage(FTexture *tex, int translation, int flags)
{
if (!tex->isHardwareCanvas())
{
FTextureBuffer texbuffer = tex->CreateTexBuffer(translation, flags | CTF_ProcessData);
bool indexed = flags & CTF_Indexed;
CreateTexture(texbuffer.mWidth, texbuffer.mHeight,indexed? 1 : 4, indexed? VK_FORMAT_R8_UNORM : VK_FORMAT_B8G8R8A8_UNORM, texbuffer.mBuffer, !indexed);
if (gl_async_textures && tex->GetImage())
{
// Create the texture now as that's easier to deal with elsewhere.
FTextureBuffer texbuffer = tex->CreateTexBuffer(translation, flags | CTF_CheckOnly);
bool indexed = flags & CTF_Indexed;
CreateTexture(texbuffer.mWidth, texbuffer.mHeight, indexed ? 1 : 4, indexed ? VK_FORMAT_R8_UNORM : VK_FORMAT_B8G8R8A8_UNORM, texbuffer.mBuffer, !indexed);
auto textureManager = fb->GetTextureManager();
int uploadID = textureManager->CreateUploadID(this);
textureManager->RunOnWorkerThread([=]() {
// Load the texture on the worker thread
auto imagedata = std::make_shared<FTextureBuffer>(tex->CreateTexBuffer(translation, flags | CTF_ProcessData));
textureManager->RunOnMainThread([=]() {
// Upload the texture on the main thread, as long as the hwrenderer didn't destroy this hwtexture already.
if (textureManager->CheckUploadID(uploadID))
{
UploadTexture(imagedata->mWidth, imagedata->mHeight, indexed ? 1 : 4, indexed ? VK_FORMAT_R8_UNORM : VK_FORMAT_B8G8R8A8_UNORM, imagedata->mBuffer, !indexed);
}
});
});
}
else
{
FTextureBuffer texbuffer = tex->CreateTexBuffer(translation, flags | CTF_ProcessData);
bool indexed = flags & CTF_Indexed;
CreateTexture(texbuffer.mWidth, texbuffer.mHeight, indexed ? 1 : 4, indexed ? VK_FORMAT_R8_UNORM : VK_FORMAT_B8G8R8A8_UNORM, texbuffer.mBuffer, !indexed);
}
}
else
{
@ -149,7 +181,12 @@ void VkHardwareTexture::CreateTexture(int w, int h, int pixelsize, VkFormat form
.Create(fb->GetDevice());
uint8_t *data = (uint8_t*)stagingBuffer->Map(0, totalSize);
memcpy(data, pixels, totalSize);
if (pixels)
memcpy(data, pixels, totalSize);
else
memset(data, 0, totalSize);
stagingBuffer->Unmap();
mImage.Image = ImageBuilder()
@ -186,6 +223,45 @@ void VkHardwareTexture::CreateTexture(int w, int h, int pixelsize, VkFormat form
fb->GetCommands()->WaitForCommands(false, true);
}
void VkHardwareTexture::UploadTexture(int w, int h, int pixelsize, VkFormat format, const void* pixels, bool mipmap)
{
if (w <= 0 || h <= 0)
throw CVulkanError("Trying to create zero size texture");
int totalSize = w * h * pixelsize;
auto stagingBuffer = BufferBuilder()
.Size(totalSize)
.Usage(VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_ONLY)
.DebugName("VkHardwareTexture.mStagingBuffer")
.Create(fb->GetDevice());
uint8_t* data = (uint8_t*)stagingBuffer->Map(0, totalSize);
memcpy(data, pixels, totalSize);
stagingBuffer->Unmap();
auto cmdbuffer = fb->GetCommands()->GetTransferCommands();
VkImageTransition()
.AddImage(&mImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, true)
.Execute(cmdbuffer);
VkBufferImageCopy region = {};
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.layerCount = 1;
region.imageExtent.depth = 1;
region.imageExtent.width = w;
region.imageExtent.height = h;
cmdbuffer->copyBufferToImage(stagingBuffer->buffer, mImage.Image->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
if (mipmap) mImage.GenerateMipmaps(cmdbuffer);
// If we queued more than 64 MB of data already: wait until the uploads finish before continuing
fb->GetCommands()->TransferDeleteList->Add(std::move(stagingBuffer));
if (fb->GetCommands()->TransferDeleteList->TotalSize > 64 * 1024 * 1024)
fb->GetCommands()->WaitForCommands(false, true);
}
int VkHardwareTexture::GetMipLevels(int w, int h)
{
int levels = 1;

View file

@ -49,6 +49,7 @@ private:
void CreateImage(FTexture *tex, int translation, int flags);
void CreateTexture(int w, int h, int pixelsize, VkFormat format, const void *pixels, bool mipmap);
void UploadTexture(int w, int h, int pixelsize, VkFormat format, const void* pixels, bool mipmap);
static int GetMipLevels(int w, int h);
VkTextureImage mImage;

View file

@ -35,10 +35,12 @@ VkTextureManager::VkTextureManager(VulkanRenderDevice* fb) : fb(fb)
CreateLightmap();
CreateIrradiancemap();
CreatePrefiltermap();
StartWorkerThread();
}
VkTextureManager::~VkTextureManager()
{
StopWorkerThread();
while (!Textures.empty())
RemoveTexture(Textures.back());
while (!PPTextures.empty())
@ -72,6 +74,16 @@ void VkTextureManager::RemoveTexture(VkHardwareTexture* texture)
texture->Reset();
texture->fb = nullptr;
Textures.erase(texture->it);
// Make sure no pending uploads access the texture after it has been destroyed by the hwrenderer
for (auto it = PendingUploads.begin(); it != PendingUploads.end(); ++it)
{
if (it->second == texture)
{
PendingUploads.erase(it);
break;
}
}
}
void VkTextureManager::AddPPTexture(VkPPTexture* texture)
@ -567,3 +579,100 @@ void VkTextureManager::DownloadLightmap(int arrayIndex, uint16_t* buffer)
memcpy(buffer, srcdata, totalSize * sizeof(uint16_t));
stagingBuffer->Unmap();
}
int VkTextureManager::CreateUploadID(VkHardwareTexture* tex)
{
int id = NextUploadID++;
PendingUploads[id] = tex;
return id;
}
bool VkTextureManager::CheckUploadID(int id)
{
auto it = PendingUploads.find(id);
if (it == PendingUploads.end())
return false;
PendingUploads.erase(it);
return true;
}
void VkTextureManager::RunOnWorkerThread(std::function<void()> task)
{
std::unique_lock lock(Worker.Mutex);
Worker.WorkerTasks.push_back(std::move(task));
lock.unlock();
Worker.CondVar.notify_one();
}
void VkTextureManager::RunOnMainThread(std::function<void()> task)
{
std::unique_lock lock(Worker.Mutex);
Worker.MainTasks.push_back(std::move(task));
}
void VkTextureManager::StartWorkerThread()
{
Worker.Thread = std::thread([this]() { WorkerThreadMain(); });
}
void VkTextureManager::StopWorkerThread()
{
std::unique_lock lock(Worker.Mutex);
Worker.StopFlag = true;
lock.unlock();
Worker.CondVar.notify_all();
Worker.Thread.join();
lock.lock();
Worker.WorkerTasks.clear();
Worker.MainTasks.clear();
Worker.StopFlag = false;
}
void VkTextureManager::ProcessMainThreadTasks()
{
std::unique_lock lock(Worker.Mutex);
std::vector<std::function<void()>> tasks;
tasks.swap(Worker.MainTasks);
lock.unlock();
for (auto& task : tasks)
{
task();
}
}
void VkTextureManager::WorkerThreadMain()
{
std::unique_lock lock(Worker.Mutex);
while (true)
{
Worker.CondVar.wait(lock, [&] { return Worker.StopFlag || !Worker.WorkerTasks.empty(); });
if (Worker.StopFlag)
break;
std::function<void()> task;
if (!Worker.WorkerTasks.empty())
{
task = std::move(Worker.WorkerTasks.front());
Worker.WorkerTasks.pop_front();
}
if (task)
{
lock.unlock();
try
{
task();
}
catch (...)
{
auto exception = std::current_exception();
RunOnMainThread([=]() { std::rethrow_exception(exception); });
}
lock.lock();
}
}
}

View file

@ -4,6 +4,10 @@
#include <zvulkan/vulkanobjects.h>
#include "vulkan/textures/vk_imagetransition.h"
#include <list>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <unordered_map>
class VulkanRenderDevice;
class VkHardwareTexture;
@ -56,6 +60,13 @@ public:
static const int MAX_REFLECTION_LOD = 4; // Note: must match what lightmodel_pbr.glsl expects
void ProcessMainThreadTasks();
void RunOnWorkerThread(std::function<void()> task);
void RunOnMainThread(std::function<void()> task);
int CreateUploadID(VkHardwareTexture* tex);
bool CheckUploadID(int id);
private:
void CreateNullTexture();
void CreateBrdfLutTexture();
@ -64,6 +75,10 @@ private:
void CreateIrradiancemap();
void CreatePrefiltermap();
void StartWorkerThread();
void StopWorkerThread();
void WorkerThreadMain();
VkPPTexture* GetVkTexture(PPTexture* texture);
VulkanRenderDevice* fb = nullptr;
@ -76,4 +91,17 @@ private:
std::unique_ptr<VulkanImage> BrdfLutTexture;
std::unique_ptr<VulkanImageView> BrdfLutTextureView;
int NextUploadID = 1;
std::unordered_map<int, VkHardwareTexture*> PendingUploads;
struct
{
std::thread Thread;
std::mutex Mutex;
std::condition_variable CondVar;
bool StopFlag = false;
std::list<std::function<void()>> WorkerTasks;
std::vector<std::function<void()>> MainTasks;
} Worker;
};

View file

@ -539,6 +539,7 @@ void VulkanRenderDevice::BeginFrame()
hwtexturecount = mTextureManager->GetHWTextureCount();
GetRenderPassManager()->ProcessMainThreadTasks();
GetTextureManager()->ProcessMainThreadTasks();
if (levelMeshChanged)
{

View file

@ -51,6 +51,7 @@
#include "imagehelpers.h"
#include "v_video.h"
#include "v_font.h"
#include <mutex>
// Wrappers to keep the definitions of these classes out of here.
IHardwareTexture* CreateHardwareTexture(int numchannels);
@ -325,6 +326,11 @@ bool FTexture::ProcessData(unsigned char* buffer, int w, int h, bool ispatch)
FTextureBuffer FTexture::CreateTexBuffer(int translation, int flags)
{
// Textures may be loaded on the main thread or on a worker thread.
// To keep things simple, make sure only one of the threads is doing this at any given time.
static std::mutex mutex;
std::unique_lock lock(mutex);
FTextureBuffer result;
if (flags & CTF_Indexed)
{