1
0
forked from mirrors/0ad

Font: make atlas uploads queue-aware on Vulkan

Goal
----
Avoid corrupting the dynamic font-atlas on Vulkan by blocking any
re-uploads until the command buffer that created / last updated the
texture has actually been submitted.

What changed
------------
* **Queue-aware textures**
  * Added `queueSubmitAware` flag to `IDevice::CreateTexture*` APIs.
  * `Vulkan::CTexture` now stores two booleans:
      - `m_QueueSubmitAware` – opt-in per texture.
      - `m_PendingQueueSubmit` – set to *true* the moment an upload
        is recorded, cleared once the submit scheduler has flushed.
  * `CRingCommandContext::ScheduleUpload` marks the texture as
    pending (`SetPendingQueueSubmit(true)`).

* **Device-side watcher**
  * `Vulkan::CDevice` keeps a
    `m_TextureUploadWatcherQueue`. Each frame it checks textures that
    were uploaded ≥ `NUMBER_OF_FRAMES_IN_FLIGHT` frames ago and
    clears their pending flag.
  * New helpers
    `ScheduleTextureUploadWatch `,
    `ProcessTextureUploadWatchQueue()`.

* **Font code**
  * Atlas texture is now created with
    `queueSubmitAware = true`.
  * `CFont::UploadTextureAtlasToGPU()` early-outs when
    `IsPendingQueueSubmit()` returns *true*, instead of tracking a
    submit-handle or the manual `m_IsLoadingTextureToGPU` flag
    (removed).

Why this is better
------------------
The logic to wait for a flush is localised inside the rendering
backend, so `CFont` only needs to ask *“is my texture busy?”*.
This removes the fragile submit-handle bookkeeping and works even if
the scheduler issues multiple submits per frame in future.

Result
------
Atlas uploads are deferred until the previous submit completes,
eliminating the intermittent glyph corruption on the Vulkan backend
while leaving GL and the dummy backend unchanged.
This commit is contained in:
trompetin17
2025-07-08 10:30:54 -05:00
parent ddcb844bcc
commit 256dff7fd4
16 changed files with 93 additions and 34 deletions
+4 -7
View File
@@ -26,7 +26,9 @@
#include "ps/Profiler2.h"
#include "ps/containers/Span.h"
#include "renderer/Renderer.h"
#include "renderer/backend/Backend.h"
#include "renderer/backend/IDevice.h"
#include "renderer/backend/vulkan/Device.h"
#include "renderer/backend/IDeviceCommandContext.h"
#include "renderer/backend/ITexture.h"
#include "renderer/backend/Sampler.h"
@@ -288,7 +290,7 @@ bool CFont::ConstructTextureAtlas()
Renderer::Backend::ITexture::Usage::TRANSFER_DST |
Renderer::Backend::ITexture::Usage::SAMPLED,
m_TextureFormat,
textureSize, textureSize, defaultSamplerDesc
textureSize, textureSize, defaultSamplerDesc, 1, 1, true
));
if (!m_Texture)
@@ -297,8 +299,6 @@ bool CFont::ConstructTextureAtlas()
return false;
}
m_IsLoadingTextureToGPU = true;
// Initialise texture with transparency, for the areas we don't
// overwrite with uploading later.
m_TexData = std::make_unique<u8[]>(m_AtlasSize);
@@ -493,10 +493,7 @@ std::optional<CVector2D> CFont::GenerateGlyphBitmap(FT_Glyph& glyph, u16 codepoi
void CFont::UploadTextureAtlasToGPU()
{
if (std::exchange(m_IsLoadingTextureToGPU, false))
return;
if (!m_IsDirty)
if (m_Texture->GetBackendTexture()->IsPendingQueueSubmit() || !m_IsDirty)
return;
Renderer::Backend::IDeviceCommandContext* deviceCommandContext = g_Renderer.GetDeviceCommandContext();
-1
View File
@@ -162,7 +162,6 @@ private:
int m_AtlasPadding;
bool m_IsDirty{false};
bool m_IsLoadingTextureToGPU{false};
float m_StrokeWidth{0.0f};
float m_Scale{1.0f};
+2 -2
View File
@@ -122,12 +122,12 @@ public:
virtual std::unique_ptr<ITexture> CreateTexture(
const char* name, const ITexture::Type type, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount) = 0;
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount, const bool queueSubmitAware = false) = 0;
virtual std::unique_ptr<ITexture> CreateTexture2D(
const char* name, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount = 1, const uint32_t sampleCount = 1) = 0;
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount = 1, const uint32_t sampleCount = 1, const bool queueSubmitAware = false) = 0;
/**
* @see IFramebuffer
+3 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2024 Wildfire Games.
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -58,6 +58,8 @@ public:
virtual uint32_t GetWidth() const = 0;
virtual uint32_t GetHeight() const = 0;
virtual uint32_t GetMIPLevelCount() const = 0;
virtual bool IsPendingQueueSubmit() const = 0;
};
} // namespace Backend
+3 -3
View File
@@ -97,7 +97,7 @@ std::unique_ptr<ITexture> CDevice::CreateTexture(
const char* /*name*/, const CTexture::Type type, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& /*defaultSamplerDesc*/, const uint32_t MIPLevelCount,
const uint32_t /*sampleCount*/)
const uint32_t /*sampleCount*/, const bool /*queueSubmitAware*/)
{
return CTexture::Create(this, type, usage, format, width, height, MIPLevelCount);
}
@@ -105,10 +105,10 @@ std::unique_ptr<ITexture> CDevice::CreateTexture(
std::unique_ptr<ITexture> CDevice::CreateTexture2D(
const char* name, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount)
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount, const bool queueSubmitAware)
{
return CreateTexture(name, ITexture::Type::TEXTURE_2D, usage,
format, width, height, defaultSamplerDesc, MIPLevelCount, sampleCount);
format, width, height, defaultSamplerDesc, MIPLevelCount, sampleCount, queueSubmitAware);
}
std::unique_ptr<IFramebuffer> CDevice::CreateFramebuffer(
+2 -2
View File
@@ -69,12 +69,12 @@ public:
std::unique_ptr<ITexture> CreateTexture(
const char* name, const ITexture::Type type, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount) override;
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount, const bool queueSubmitAware = false) override;
std::unique_ptr<ITexture> CreateTexture2D(
const char* name, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount = 1, const uint32_t sampleCount = 1) override;
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount = 1, const uint32_t sampleCount = 1, const bool queueSubmitAware = false) override;
std::unique_ptr<IFramebuffer> CreateFramebuffer(
const char* name, SColorAttachment* colorAttachment,
+4 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2022 Wildfire Games.
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -51,6 +51,9 @@ public:
uint32_t GetHeight() const override { return m_Height; }
uint32_t GetMIPLevelCount() const override { return m_MIPLevelCount; }
// Dummy backend does not support queue submission.
bool IsPendingQueueSubmit() const override { return false; }
private:
friend class CDevice;
+3 -3
View File
@@ -908,7 +908,7 @@ std::unique_ptr<IVertexInputLayout> CDevice::CreateVertexInputLayout(
std::unique_ptr<ITexture> CDevice::CreateTexture(
const char* name, const ITexture::Type type, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount)
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount, const bool /*queueSubmitAware*/)
{
return CTexture::Create(this, name, type, usage,
format, width, height, defaultSamplerDesc, MIPLevelCount, sampleCount);
@@ -917,10 +917,10 @@ std::unique_ptr<ITexture> CDevice::CreateTexture(
std::unique_ptr<ITexture> CDevice::CreateTexture2D(
const char* name, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount)
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount, const bool queueSubmitAware)
{
return CreateTexture(name, CTexture::Type::TEXTURE_2D, usage,
format, width, height, defaultSamplerDesc, MIPLevelCount, sampleCount);
format, width, height, defaultSamplerDesc, MIPLevelCount, sampleCount, queueSubmitAware);
}
std::unique_ptr<IFramebuffer> CDevice::CreateFramebuffer(
+3 -3
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2024 Wildfire Games.
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -84,12 +84,12 @@ public:
std::unique_ptr<ITexture> CreateTexture(
const char* name, const ITexture::Type type, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount) override;
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount, const bool queueSubmitAware) override;
std::unique_ptr<ITexture> CreateTexture2D(
const char* name, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount = 1, const uint32_t sampleCount = 1) override;
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount = 1, const uint32_t sampleCount = 1, const bool queueSubmitAware = false) override;
std::unique_ptr<IFramebuffer> CreateFramebuffer(
const char* name, SColorAttachment* colorAttachment,
+3 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2022 Wildfire Games.
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -58,6 +58,8 @@ public:
GLuint GetHandle() const { return m_Handle; }
// GL doesn's support queue submmit, so we don't need to track pending.
bool IsPendingQueueSubmit() const override { return false; }
private:
friend class CDevice;
+27 -4
View File
@@ -677,6 +677,7 @@ CDevice::~CDevice()
if (m_QueryPool)
vkDestroyQueryPool(GetVkDevice(), m_QueryPool, nullptr);
ProcessTextureUploadWatchQueue(true);
ProcessDeviceObjectToDestroyQueue(true);
m_RenderPassManager.reset();
@@ -749,21 +750,21 @@ std::unique_ptr<IVertexInputLayout> CDevice::CreateVertexInputLayout(
std::unique_ptr<ITexture> CDevice::CreateTexture(
const char* name, const ITexture::Type type, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount)
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount, const bool queueSubmitAware)
{
return CTexture::Create(
this, name, type, usage, format, width, height,
defaultSamplerDesc, MIPLevelCount, sampleCount);
defaultSamplerDesc, MIPLevelCount, sampleCount, queueSubmitAware);
}
std::unique_ptr<ITexture> CDevice::CreateTexture2D(
const char* name, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount)
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount, const bool queueSubmitAware)
{
return CreateTexture(
name, ITexture::Type::TEXTURE_2D, usage, format,
width, height, defaultSamplerDesc, MIPLevelCount, sampleCount);
width, height, defaultSamplerDesc, MIPLevelCount, sampleCount, queueSubmitAware);
}
std::unique_ptr<IFramebuffer> CDevice::CreateFramebuffer(
@@ -832,6 +833,7 @@ void CDevice::Present()
m_SubmitScheduler->Present(*m_SwapChain);
ProcessTextureUploadWatchQueue();
ProcessObjectToDestroyQueue();
ProcessDeviceObjectToDestroyQueue();
@@ -1012,6 +1014,10 @@ void CDevice::ScheduleBufferToDestroy(const DeviceObjectUID uid)
{
m_BufferToDestroyQueue.push({m_FrameID, uid});
}
void CDevice::ScheduleTextureUploadWatch(CTexture* texture)
{
m_TextureUploadWatcherQueue.push({ m_FrameID, texture });
}
void CDevice::SetObjectName(VkObjectType type, const uint64_t handle, const char* name)
{
@@ -1115,6 +1121,18 @@ void CDevice::ProcessDeviceObjectToDestroyQueue(const bool ignoreFrameID)
}
}
void CDevice::ProcessTextureUploadWatchQueue(const bool ignoreFrameID)
{
while (!m_TextureUploadWatcherQueue.empty() &&
(ignoreFrameID || m_TextureUploadWatcherQueue.front().first + NUMBER_OF_FRAMES_IN_FLIGHT < m_FrameID))
{
CTexture* texture = m_TextureUploadWatcherQueue.front().second;
if (texture)
texture->SetPendingQueueSubmit(false);
m_TextureUploadWatcherQueue.pop();
}
}
CTexture* CDevice::GetCurrentBackbufferTexture()
{
return IsSwapChainValid() ? m_SwapChain->GetCurrentBackbufferTexture() : nullptr;
@@ -1147,6 +1165,11 @@ std::unique_ptr<IDevice> CreateDevice(SDL_Window* window)
return Vulkan::CDevice::Create(window);
}
uint32_t CDevice::GetCurrentSchedulerHandle() const
{
return m_SubmitScheduler ? m_SubmitScheduler->GetCurrentHandle() : CSubmitScheduler::INVALID_SUBMIT_HANDLE;
}
} // namespace Vulkan
} // namespace Backend
+9 -2
View File
@@ -56,6 +56,7 @@ class CRingCommandContext;
class CSamplerManager;
class CSubmitScheduler;
class CSwapChain;
class CTexture;
class CDevice final : public IDevice
{
@@ -90,12 +91,12 @@ public:
std::unique_ptr<ITexture> CreateTexture(
const char* name, const ITexture::Type type, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount) override;
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount, const uint32_t sampleCount, const bool queueSubmitAware = false) override;
std::unique_ptr<ITexture> CreateTexture2D(
const char* name, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount = 1, const uint32_t sampleCount = 1) override;
const Sampler::Desc& defaultSamplerDesc, const uint32_t MIPLevelCount = 1, const uint32_t sampleCount = 1, const bool queueSubmitAware = false) override;
std::unique_ptr<IFramebuffer> CreateFramebuffer(
const char* name, SColorAttachment* colorAttachment,
@@ -159,6 +160,8 @@ public:
void ScheduleBufferToDestroy(const DeviceObjectUID uid);
void ScheduleTextureUploadWatch(CTexture* texture);
void SetObjectName(VkObjectType type, const void* handle, const char* name)
{
SetObjectName(type, reinterpret_cast<uint64_t>(handle), name);
@@ -182,6 +185,8 @@ public:
DeviceObjectUID GenerateNextDeviceObjectUID();
uint32_t GetCurrentSchedulerHandle() const;
private:
CDevice();
@@ -189,6 +194,7 @@ private:
bool IsSwapChainValid();
void ProcessObjectToDestroyQueue(const bool ignoreFrameID = false);
void ProcessDeviceObjectToDestroyQueue(const bool ignoreFrameID = false);
void ProcessTextureUploadWatchQueue(const bool ignoreFrameID = false);
bool IsFormatSupportedForUsage(const Format format, const uint32_t usage) const;
@@ -239,6 +245,7 @@ private:
};
std::queue<ObjectToDestroy> m_ObjectToDestroyQueue;
std::queue<std::pair<uint32_t, DeviceObjectUID>> m_TextureToDestroyQueue;
std::queue<std::pair<uint32_t, CTexture*>> m_TextureUploadWatcherQueue;
std::queue<std::pair<uint32_t, DeviceObjectUID>> m_BufferToDestroyQueue;
std::unique_ptr<CRenderPassManager> m_RenderPassManager;
@@ -164,6 +164,7 @@ void CRingCommandContext::ScheduleUpload(
const uint32_t level, const uint32_t layer)
{
ENSURE(texture->GetType() != ITexture::Type::TEXTURE_2D_MULTISAMPLE);
const Format format = texture->GetFormat();
if (texture->GetType() != ITexture::Type::TEXTURE_CUBE)
ENSURE(layer == 0);
@@ -223,6 +224,7 @@ void CRingCommandContext::ScheduleUpload(
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask);
texture->SetInitialized();
texture->SetPendingQueueSubmit(true);
}
void CRingCommandContext::ScheduleUpload(
@@ -65,6 +65,7 @@ public:
void Flush();
SubmitHandle GetCurrentHandle() const { return m_CurrentHandle; };
private:
CSubmitScheduler(CDevice* device, VkQueue queue);
+14 -2
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2024 Wildfire Games.
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -24,6 +24,8 @@
#include "renderer/backend/vulkan/SamplerManager.h"
#include "renderer/backend/vulkan/Utilities.h"
#include <utility>
namespace Renderer
{
@@ -38,7 +40,7 @@ std::unique_ptr<CTexture> CTexture::Create(
CDevice* device, const char* name, const Type type, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& defaultSamplerDesc,
const uint32_t MIPLevelCount, const uint32_t sampleCount)
const uint32_t MIPLevelCount, const uint32_t sampleCount, const bool queueSubmitAware)
{
std::unique_ptr<CTexture> texture(new CTexture(device));
@@ -50,6 +52,7 @@ std::unique_ptr<CTexture> CTexture::Create(
texture->m_MIPLevelCount = MIPLevelCount;
texture->m_SampleCount = sampleCount;
texture->m_LayerCount = type == ITexture::Type::TEXTURE_CUBE ? 6 : 1;
texture->m_QueueSubmitAware = queueSubmitAware;
if (type == Type::TEXTURE_2D_MULTISAMPLE)
ENSURE(sampleCount > 1);
@@ -376,6 +379,15 @@ CTexture::~CTexture()
m_Device->ScheduleTextureToDestroy(m_UID);
}
void CTexture::SetPendingQueueSubmit(bool pending)
{
if (!m_QueueSubmitAware)
return;
if (!std::exchange(m_PendingQueueSubmit, pending) && pending)
m_Device->ScheduleTextureUploadWatch(this);
}
IDevice* CTexture::GetDevice()
{
return m_Device;
+13 -2
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2023 Wildfire Games.
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -54,6 +54,9 @@ public:
uint32_t GetSampleCount() const { return m_SampleCount; }
uint32_t GetLayerCount() const { return m_LayerCount; }
bool IsPendingQueueSubmit() const override { return m_PendingQueueSubmit; }
void SetPendingQueueSubmit(bool pending);
VkImage GetImage() { return m_Image; }
VkImageView GetAttachmentImageView() { return m_AttachmentImageView; }
VkImageView GetSamplerImageView() { return m_SamplerImageView; }
@@ -86,7 +89,7 @@ private:
CDevice* device, const char* name, const Type type, const uint32_t usage,
const Format format, const uint32_t width, const uint32_t height,
const Sampler::Desc& defaultSamplerDesc,
const uint32_t MIPLevelCount, const uint32_t sampleCount);
const uint32_t MIPLevelCount, const uint32_t sampleCount, const bool queueSubmitAware = false);
static std::unique_ptr<CTexture> WrapBackbufferImage(
CDevice* device, const char* name, const VkImage image, const VkFormat format,
@@ -128,6 +131,14 @@ private:
// It's safe to store the current state while we use a single device command
// context.
bool m_Initialized = false;
// We store a flag to indicate that the texture is in Queue submit.
bool m_PendingQueueSubmit = false;
// If true, the texture is aware of queue submission useful for
// for dynamic textures that are updated frequently and needs to wait for
// the queue submission to finish before overwriting the texture data.
bool m_QueueSubmitAware = false;
};
} // namespace Vulkan