thin3d: add sub-rectangle texture updates, use them for imgui fonts

imgui 1.92 hands the backend a list of dirty rectangles when its font atlas
grows, but thin3d could only replace a whole mip level, so every new glyph
re-uploaded the entire atlas.

Adds DrawContext::UpdateTextureRegions, taking a batch of regions so each
backend can submit them together:

- Vulkan: packs the regions into the push pool and issues a single
  vkCmdCopyBufferToImage from the init command buffer, transitioning only
  the level being written.
- OpenGL: new TEXTURE_SUBIMAGE init step. The existing sub-image path is a
  render command needing an active render pass and a texture slot, which
  doesn't fit here. Rows are packed caller-side since GLES2 lacks
  GL_UNPACK_ROW_LENGTH.
- D3D11: UpdateSubresource with a box, no staging texture needed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Henrik RydgårdandClaude Opus 5 committed 2026-09-12 13:16:17 -06:00
1 parent 43850e7c03
commit 70095e458b
10 files changed
+204 -9

No files matched your search

+23
View File
@@ -112,6 +112,7 @@ public:
void UpdateBuffer(Buffer *buffer, const uint8_t *data, size_t offset, size_t size, UpdateBufferFlags flags) override;
void UpdateTextureLevels(Texture *texture, const uint8_t **data, TextureCallback initDataCallback, int numLevels) override;
void UpdateTextureRegions(Texture *texture, int level, const TextureRegionUpdate *regions, int numRegions) override;
void CopyFramebufferImage(Framebuffer *src, int level, int x, int y, int z, Framebuffer *dst, int dstLevel, int dstX, int dstY, int dstZ, int width, int height, int depth, Aspect aspects, const char *tag) override;
bool BlitFramebuffer(Framebuffer *src, int srcX1, int srcY1, int srcX2, int srcY2, Framebuffer *dst, int dstX1, int dstY1, int dstX2, int dstY2, Aspect aspects, FBBlitFilter filter, const char *tag) override;
@@ -890,6 +891,7 @@ public:
bool CreateStagingTexture(ID3D11Device *device);
void UpdateTextureLevels(ID3D11DeviceContext *context, ID3D11Device *device, Texture *texture, const uint8_t *const *data, TextureCallback initDataCallback, int numLevels);
void UpdateTextureRegions(ID3D11DeviceContext *context, int level, const TextureRegionUpdate *regions, int numRegions);
ID3D11ShaderResourceView *View() { return view_.Get(); }
@@ -1051,6 +1053,22 @@ void D3D11Texture::UpdateTextureLevels(ID3D11DeviceContext *context, ID3D11Devic
stagingTex_.Reset();
}
void D3D11Texture::UpdateTextureRegions(ID3D11DeviceContext *context, int level, const TextureRegionUpdate *regions, int numRegions) {
const UINT pixelSize = (UINT)DataFormatSizeInBytes(format_);
for (int i = 0; i < numRegions; i++) {
const TextureRegionUpdate &region = regions[i];
D3D11_BOX box{};
box.left = region.x;
box.top = region.y;
box.front = 0;
box.right = region.x + region.w;
box.bottom = region.y + region.h;
box.back = 1;
const UINT srcStride = region.byteStride ? (UINT)region.byteStride : region.w * pixelSize;
context->UpdateSubresource(tex_.Get(), level, &box, region.data, srcStride, 0);
}
}
Texture *D3D11DrawContext::CreateTexture(const TextureDesc &desc) {
if (!(GetDataFormatSupport(desc.format) & FMT_TEXTURE)) {
// D3D11 does not support this format as a texture format.
@@ -1077,6 +1095,11 @@ void D3D11DrawContext::UpdateTextureLevels(Texture *texture, const uint8_t **dat
tex->UpdateTextureLevels(context_.Get(), device_.Get(), texture, data, initDataCallback, numLevels);
}
void D3D11DrawContext::UpdateTextureRegions(Texture *texture, int level, const TextureRegionUpdate *regions, int numRegions) {
D3D11Texture *tex = (D3D11Texture *)texture;
tex->UpdateTextureRegions(context_.Get(), level, regions, numRegions);
}
ShaderModule *D3D11DrawContext::CreateShaderModule(ShaderStage stage, ShaderLanguage language, const uint8_t *data, size_t dataSize, const char *tag) {
if (language != ShaderLanguage::HLSL_D3D11) {
ERROR_LOG(Log::G3D, "Unsupported shader language");
+34
View File
@@ -130,6 +130,15 @@ void GLQueueRunner::RunInitSteps(const FastVec<GLRInitStep> &steps, bool skipGLC
}
break;
}
case GLRInitStepType::TEXTURE_SUBIMAGE:
{
if (step.texture_subimage.allocType == GLRAllocType::ALIGNED) {
FreeAlignedMemory(step.texture_subimage.data);
} else if (step.texture_subimage.allocType == GLRAllocType::NEW) {
delete[] step.texture_subimage.data;
}
break;
}
case GLRInitStepType::CREATE_PROGRAM:
{
WARN_LOG(Log::G3D, "CREATE_PROGRAM found with skipGLCalls, not good");
@@ -400,6 +409,31 @@ void GLQueueRunner::RunInitSteps(const FastVec<GLRInitStep> &steps, bool skipGLC
CHECK_GL_ERROR_IF_DEBUG();
break;
}
case GLRInitStepType::TEXTURE_SUBIMAGE:
{
GLRTexture *tex = step.texture_subimage.texture;
CHECK_GL_ERROR_IF_DEBUG();
if (boundTexture != tex->texture) {
glBindTexture(tex->target, tex->texture);
boundTexture = tex->texture;
}
_assert_(tex->target == GL_TEXTURE_2D);
_assert_(step.texture_subimage.data != nullptr);
GLenum internalFormat, format, type;
int alignment;
Thin3DFormatToGLFormatAndType(step.texture_subimage.format, internalFormat, format, type, alignment);
glTexSubImage2D(tex->target, step.texture_subimage.level,
step.texture_subimage.x, step.texture_subimage.y,
step.texture_subimage.width, step.texture_subimage.height,
format, type, step.texture_subimage.data);
if (step.texture_subimage.allocType == GLRAllocType::ALIGNED) {
FreeAlignedMemory(step.texture_subimage.data);
} else if (step.texture_subimage.allocType == GLRAllocType::NEW) {
delete[] step.texture_subimage.data;
}
CHECK_GL_ERROR_IF_DEBUG();
break;
}
case GLRInitStepType::TEXTURE_FINALIZE:
{
CHECK_GL_ERROR_IF_DEBUG();
+12
View File
@@ -214,6 +214,7 @@ enum class GLRInitStepType : uint8_t {
CREATE_FRAMEBUFFER,
TEXTURE_IMAGE,
TEXTURE_SUBIMAGE,
TEXTURE_FINALIZE,
BUFFER_SUBDATA,
};
@@ -266,6 +267,17 @@ struct GLRInitStep {
bool linearFilter;
uint8_t *data; // owned, delete[]-d
} texture_image;
struct {
GLRTexture *texture;
Draw::DataFormat format;
int level;
uint16_t x;
uint16_t y;
uint16_t width;
uint16_t height;
GLRAllocType allocType;
uint8_t *data; // owned, delete[]-d
} texture_subimage;
struct {
GLRTexture *texture;
int loadedLevels;
+17
View File
@@ -444,6 +444,23 @@ public:
step.texture_image.linearFilter = linearFilter;
}
// Takes ownership over the data pointer and delete[]-s it. Runs as an init step, so unlike
// TextureSubImage below, it doesn't have to happen inside a render pass.
void TextureSubImageInit(GLRTexture *texture, int level, int x, int y, int width, int height, Draw::DataFormat format, uint8_t *data, GLRAllocType allocType = GLRAllocType::NEW) {
std::lock_guard<std::mutex> lock(initStepsMutex_);
GLRInitStep &step = initSteps_.push_uninitialized();
step.stepType = GLRInitStepType::TEXTURE_SUBIMAGE;
step.texture_subimage.texture = texture;
step.texture_subimage.data = data;
step.texture_subimage.format = format;
step.texture_subimage.level = level;
step.texture_subimage.x = x;
step.texture_subimage.y = y;
step.texture_subimage.width = width;
step.texture_subimage.height = height;
step.texture_subimage.allocType = allocType;
}
void TextureSubImage(int slot, GLRTexture *texture, int level, int x, int y, int width, int height, Draw::DataFormat format, uint8_t *data, GLRAllocType allocType = GLRAllocType::NEW) {
_dbg_assert_(curRenderStep_ && curRenderStep_->stepType == GLRStepType::RENDER);
GLRRenderData _data(GLRRenderCommand::TEXTURE_SUBIMAGE);
+24
View File
@@ -377,6 +377,7 @@ public:
void UpdateBuffer(Buffer *buffer, const uint8_t *data, size_t offset, size_t size, UpdateBufferFlags flags) override;
void UpdateTextureLevels(Texture *texture, const uint8_t **data, TextureCallback initDataCallback, int numLevels) override;
void UpdateTextureRegions(Texture *texture, int level, const TextureRegionUpdate *regions, int numRegions) override;
void CopyFramebufferImage(Framebuffer *src, int level, int x, int y, int z, Framebuffer *dst, int dstLevel, int dstX, int dstY, int dstZ, int width, int height, int depth, Aspect aspects, const char *tag) override;
bool BlitFramebuffer(Framebuffer *src, int srcX1, int srcY1, int srcX2, int srcY2, Framebuffer *dst, int dstX1, int dstY1, int dstX2, int dstY2, Aspect aspects, FBBlitFilter filter, const char *tag) override;
@@ -860,6 +861,7 @@ public:
}
void UpdateTextureLevels(GLRenderManager *render, const uint8_t *const *data, int numLevels, TextureCallback initDataCallback);
void UpdateTextureRegions(GLRenderManager *render, int level, const TextureRegionUpdate *regions, int numRegions);
private:
void SetImageData(int x, int y, int z, int width, int height, int depth, int level, int stride, const uint8_t *data, TextureCallback initDataCallback);
@@ -920,6 +922,23 @@ void OpenGLTexture::UpdateTextureLevels(GLRenderManager *render, const uint8_t *
render->FinalizeTexture(tex_, mipLevels_, genMips);
}
void OpenGLTexture::UpdateTextureRegions(GLRenderManager *render, int level, const TextureRegionUpdate *regions, int numRegions) {
const int pixelSize = (int)DataFormatSizeInBytes(format_);
for (int i = 0; i < numRegions; i++) {
const TextureRegionUpdate &region = regions[i];
_dbg_assert_(region.w > 0 && region.h > 0);
const int srcStride = region.byteStride ? region.byteStride : region.w * pixelSize;
const int dstStride = region.w * pixelSize;
// glTexSubImage2D could take the stride through GL_UNPACK_ROW_LENGTH, but that's not in GLES2,
// and the queue runner owns the data anyway, so just pack it here.
uint8_t *texData = new uint8_t[(size_t)dstStride * region.h];
for (int y = 0; y < region.h; y++) {
memcpy(texData + (size_t)dstStride * y, region.data + (size_t)srcStride * y, dstStride);
}
render->TextureSubImageInit(tex_, level, region.x, region.y, region.w, region.h, format_, texData);
}
}
OpenGLTexture::~OpenGLTexture() {
if (tex_) {
render_->DeleteTexture(tex_);
@@ -1045,6 +1064,11 @@ void OpenGLContext::UpdateTextureLevels(Texture *texture, const uint8_t **data,
tex->UpdateTextureLevels(&renderManager_, data, numLevels, initDataCallback);
}
void OpenGLContext::UpdateTextureRegions(Texture *texture, int level, const TextureRegionUpdate *regions, int numRegions) {
OpenGLTexture *tex = (OpenGLTexture *)texture;
tex->UpdateTextureRegions(&renderManager_, level, regions, numRegions);
}
DepthStencilState *OpenGLContext::CreateDepthStencilState(const DepthStencilStateDesc &desc) {
OpenGLDepthStencilState *ds = new OpenGLDepthStencilState();
ds->depthTestEnabled = desc.depthTestEnabled;
+8 -4
View File
@@ -170,15 +170,19 @@ bool VulkanTexture::CreateDirect(int w, int h, int depth, int numMips, VkFormat
}
void VulkanTexture::CopyBufferToMipLevel(VkCommandBuffer cmd, TextureCopyBatch *copyBatch, int mip, int mipWidth, int mipHeight, int depthLayer, VkBuffer buffer, uint32_t offset, size_t rowLength) {
CopyBufferToMipLevelRegion(cmd, copyBatch, mip, 0, 0, mipWidth, mipHeight, depthLayer, buffer, offset, rowLength);
}
void VulkanTexture::CopyBufferToMipLevelRegion(VkCommandBuffer cmd, TextureCopyBatch *copyBatch, int mip, int x, int y, int w, int h, int depthLayer, VkBuffer buffer, uint32_t offset, size_t rowLength) {
VkBufferImageCopy &copy_region = copyBatch->copies.push_uninitialized();
copy_region.bufferOffset = offset;
copy_region.bufferRowLength = (uint32_t)rowLength;
copy_region.bufferImageHeight = 0; // 2D
copy_region.imageOffset.x = 0;
copy_region.imageOffset.y = 0;
copy_region.imageOffset.x = x;
copy_region.imageOffset.y = y;
copy_region.imageOffset.z = depthLayer;
copy_region.imageExtent.width = mipWidth;
copy_region.imageExtent.height = mipHeight;
copy_region.imageExtent.width = w;
copy_region.imageExtent.height = h;
copy_region.imageExtent.depth = 1;
copy_region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
copy_region.imageSubresource.mipLevel = mip;
+2
View File
@@ -36,6 +36,8 @@ public:
// Can also be used to copy individual levels of a 3D texture.
// If possible, will just add to the batch instead of submitting a copy.
void CopyBufferToMipLevel(VkCommandBuffer cmd, TextureCopyBatch *copyBatch, int mip, int mipWidth, int mipHeight, int depthLayer, VkBuffer buffer, uint32_t offset, size_t rowLength); // rowLength is in pixels
// Same, but only covers a rectangle of the mip level instead of all of it.
void CopyBufferToMipLevelRegion(VkCommandBuffer cmd, TextureCopyBatch *copyBatch, int mip, int x, int y, int w, int h, int depthLayer, VkBuffer buffer, uint32_t offset, size_t rowLength); // rowLength is in pixels
void FinishCopyBatch(VkCommandBuffer cmd, TextureCopyBatch *copyBatch);
void GenerateMips(VkCommandBuffer cmd, int firstMipToGenerate, bool fromCompute);
+50
View File
@@ -332,6 +332,7 @@ public:
}
bool Create(VkCommandBuffer cmd, VulkanBarrierBatch *postBarriers, VulkanPushPool *pushBuffer, const TextureDesc &desc);
void Update(VkCommandBuffer cmd, VulkanBarrierBatch *postBarriers, VulkanPushPool *pushBuffer, const uint8_t *const *data, TextureCallback callback, int numLevels);
void UpdateRegions(VkCommandBuffer cmd, VulkanBarrierBatch *postBarriers, VulkanPushPool *pushBuffer, int level, const TextureRegionUpdate *regions, int numRegions);
~VKTexture() {
Destroy();
@@ -449,6 +450,7 @@ public:
void UpdateBuffer(Buffer *buffer, const uint8_t *data, size_t offset, size_t size, UpdateBufferFlags flags) override;
void UpdateTextureLevels(Texture *texture, const uint8_t **data, TextureCallback initDataCallback, int numLevels) override;
void UpdateTextureRegions(Texture *texture, int level, const TextureRegionUpdate *regions, int numRegions) override;
void CopyFramebufferImage(Framebuffer *src, int level, int x, int y, int z, Framebuffer *dst, int dstLevel, int dstX, int dstY, int dstZ, int width, int height, int depth, Aspect aspects, const char *tag) override;
bool BlitFramebuffer(Framebuffer *src, int srcX1, int srcY1, int srcX2, int srcY2, Framebuffer *dst, int dstX1, int dstY1, int dstX2, int dstY2, Aspect aspects, FBBlitFilter filter, const char *tag) override;
@@ -860,6 +862,37 @@ void VKTexture::Update(VkCommandBuffer cmd, VulkanBarrierBatch *postBarriers, Vu
vkTex_->RestoreAfterTransferDst(numLevels, postBarriers);
}
void VKTexture::UpdateRegions(VkCommandBuffer cmd, VulkanBarrierBatch *postBarriers, VulkanPushPool *pushBuffer, int level, const TextureRegionUpdate *regions, int numRegions) {
VkFormat vulkanFormat = DataFormatToVulkan(format_);
int bpp = GetBpp(vulkanFormat);
_dbg_assert_(bpp != 0);
const int bytesPerPixel = bpp / 8;
// Only the level we're writing needs to change layout, the others stay sampleable.
vkTex_->PrepareForTransferDst(cmd, level + 1);
TextureCopyBatch batch;
batch.reserve(numRegions);
for (int i = 0; i < numRegions; i++) {
const TextureRegionUpdate &region = regions[i];
_dbg_assert_(region.w > 0 && region.h > 0);
const int srcStride = region.byteStride ? region.byteStride : region.w * bytesPerPixel;
const int dstStride = region.w * bytesPerPixel;
uint32_t offset;
VkBuffer buf;
uint8_t *dest = (uint8_t *)pushBuffer->Allocate((size_t)dstStride * region.h, 16, &buf, &offset);
_assert_(dest != nullptr);
for (int y = 0; y < region.h; y++) {
memcpy(dest + (size_t)dstStride * y, region.data + (size_t)srcStride * y, dstStride);
}
vkTex_->CopyBufferToMipLevelRegion(cmd, &batch, level, region.x, region.y, region.w, region.h, 0, buf, offset, region.w);
}
vkTex_->FinishCopyBatch(cmd, &batch);
vkTex_->RestoreAfterTransferDst(level + 1, postBarriers);
}
void VKTexture::UpdateInternal(VkCommandBuffer cmd, VulkanPushPool *pushBuffer, const uint8_t * const *data, TextureCallback initDataCallback, int numLevels) {
int w = width_;
int h = height_;
@@ -1401,6 +1434,23 @@ void VKContext::UpdateTextureLevels(Texture *texture, const uint8_t **data, Text
tex->Update(initCmd, &renderManager_.PostInitBarrier(), push_, data, initDataCallback, numLevels);
}
void VKContext::UpdateTextureRegions(Texture *texture, int level, const TextureRegionUpdate *regions, int numRegions) {
if (numRegions <= 0) {
return;
}
VkCommandBuffer initCmd = renderManager_.GetInitCmd();
if (!push_ || !initCmd) {
// Too early! Fail.
ERROR_LOG(Log::G3D, "Can't update textures before the first frame has started.");
return;
}
VKTexture *tex = (VKTexture *)texture;
_dbg_assert_(level < tex->NumLevels());
tex->UpdateRegions(initCmd, &renderManager_.PostInitBarrier(), push_, level, regions, numRegions);
}
static inline void CopySide(VkStencilOpState &dest, const StencilSetup &src) {
dest.compareOp = compToVK[(int)src.compareOp];
dest.failOp = stencilOpToVK[(int)src.failOp];
+16
View File
@@ -669,6 +669,18 @@ struct TextureDesc {
TextureCallback initDataCallback;
};
// A rectangular piece of a single mip level, for DrawContext::UpdateTextureRegions.
struct TextureRegionUpdate {
int x;
int y;
int w;
int h;
// Points at the top left pixel of the region. Does not take ownership.
const uint8_t *data;
// Distance between rows in data, in bytes. 0 means tightly packed, that is w pixels.
int byteStride;
};
enum class RPAction {
KEEP = 0,
CLEAR = 1,
@@ -790,6 +802,10 @@ public:
// while in Vulkan this might cause various strangeness like image corruption.
virtual void UpdateTextureLevels(Texture *texture, const uint8_t **data, TextureCallback initDataCallback, int numLevels) = 0;
// Replaces a set of rectangular regions of a single mip level, leaving the rest of the texture alone.
// Same in-flight caveat as UpdateTextureLevels. The regions must not overlap each other.
virtual void UpdateTextureRegions(Texture *texture, int level, const TextureRegionUpdate *regions, int numRegions) = 0;
virtual void CopyFramebufferImage(Framebuffer *src, int level, int x, int y, int z, Framebuffer *dst, int dstLevel, int dstX, int dstY, int dstZ, int width, int height, int depth, Aspect aspects, const char *tag) = 0;
virtual bool BlitFramebuffer(Framebuffer *src, int srcX1, int srcY1, int srcX2, int srcY2, Framebuffer *dst, int dstX1, int dstY1, int dstX2, int dstY2, Aspect aspects, FBBlitFilter filter, const char *tag) = 0;