From 456858d50f203cd7e9c937c4b37f27dd0824e073 Mon Sep 17 00:00:00 2001 From: Mikko Mononen Date: Thu, 23 Apr 2026 12:10:26 +0300 Subject: [PATCH] DrawList: Textured Round Corners: implemented faster render path for stroked rectangles. (1962) - added check to render simpler geometry when (stroked) rectangle has integer coords - render non.rounded with 4 non-AA rectangles - added textures for corners of 1-3 thickness and 1-16 rounding - use textured corners when possible, or fallback to generic one --- imgui.h | 7 +- imgui_draw.cpp | 270 +++++++++++++++++++++++++++++++++++++++++++++-- imgui_internal.h | 1 + 3 files changed, 269 insertions(+), 9 deletions(-) diff --git a/imgui.h b/imgui.h index cb0baa89f..ebc363e3d 100644 --- a/imgui.h +++ b/imgui.h @@ -3326,6 +3326,9 @@ struct ImGuiSelectionExternalStorage #ifndef IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX #define IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX (16) #endif +#ifndef IM_DRAWLIST_TEX_CORNERS_THICKNESS_MAX +#define IM_DRAWLIST_TEX_CORNERS_THICKNESS_MAX (3) +#endif // ImDrawIdx: vertex index. [Compile-time configurable type] // - To use 16-bit indices + allow large meshes: backend need to set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset' and handle ImDrawCmd::VtxOffset (recommended). @@ -3638,7 +3641,8 @@ struct ImDrawList IMGUI_API int _CalcCircleAutoSegmentCount(float radius) const; IMGUI_API void _PathArcToFastEx(const ImVec2& center, float radius, int a_min_sample, int a_max_sample, int a_step); IMGUI_API void _PathArcToN(const ImVec2& center, float radius, float a_min, float a_max, int num_segments); - IMGUI_API void _AddMirrored9Slice(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float r, ImVec4 tex_uvs, ImDrawFlags flags); + IMGUI_API void _AddRectFilledBaked(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float r, ImVec4 tex_uvs, ImDrawFlags flags); + IMGUI_API void _AddRectBaked(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float r, float t, ImVec4 tex_uvs, ImDrawFlags flags); }; // All draw data to render a Dear ImGui frame @@ -4001,6 +4005,7 @@ struct ImFontAtlas ImVector Sources; // Source/configuration data ImVec4 TexUvLines[IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1]; // UVs for baked anti-aliased lines ImVec4 TexUvCornerFills[IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX]; // UVs for baked anti-aliased corners + ImVec4 TexUvCornerStrokes[IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX * IM_DRAWLIST_TEX_CORNERS_THICKNESS_MAX]; // UVs for baked anti-aliased corners int TexNextUniqueID; // Next value to be stored in TexData->UniqueID int FontNextUniqueID; // Next value to be stored in ImFont->FontID ImVector DrawListSharedDatas; // List of users for this atlas. Typically one per Dear ImGui context. diff --git a/imgui_draw.cpp b/imgui_draw.cpp index dda9c6f45..062548a8e 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -1564,6 +1564,125 @@ void ImDrawList::AddLineV(float x, float min_y, float max_y, ImU32 col, float th } } + +void ImDrawList::_AddRectBaked(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float r, float t, ImVec4 tex_uvs, ImDrawFlags flags) +{ + const ImVec2 uv_tl(tex_uvs.x, tex_uvs.y); + const ImVec2 uv_tr(tex_uvs.z, tex_uvs.y); + const ImVec2 uv_br(tex_uvs.z, tex_uvs.w); + const ImVec2 uv_bl(tex_uvs.x, tex_uvs.w); + + const int idx_allocated = 8 * 2 * 3; + const int vtx_allocated = 8 * 4; + PrimReserve(idx_allocated, vtx_allocated); + ImDrawVert* start_vtx_ptr = _VtxWritePtr; + ImDrawIdx* start_idx_ptr = _IdxWritePtr; + + if (flags & ImDrawFlags_RoundCornersTopLeft) + { + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + PrimWriteVtx(ImVec2(p_min.x, p_min.y), uv_tl, col); + PrimWriteVtx(ImVec2(p_min.x + r, p_min.y), uv_tr, col); + PrimWriteVtx(ImVec2(p_min.x + r, p_min.y + r), uv_br, col); + PrimWriteVtx(ImVec2(p_min.x, p_min.y + r), uv_bl, col); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+1); PrimWriteIdx(idx+2); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+2); PrimWriteIdx(idx+3); + } + + if (flags & ImDrawFlags_RoundCornersTopRight) + { + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + PrimWriteVtx(ImVec2(p_max.x - r, p_min.y), uv_tr, col); + PrimWriteVtx(ImVec2(p_max.x, p_min.y), uv_tl, col); + PrimWriteVtx(ImVec2(p_max.x, p_min.y + r), uv_bl, col); + PrimWriteVtx(ImVec2(p_max.x - r, p_min.y + r), uv_br, col); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+1); PrimWriteIdx(idx+2); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+2); PrimWriteIdx(idx+3); + } + + if (flags & ImDrawFlags_RoundCornersBottomRight) + { + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + PrimWriteVtx(ImVec2(p_max.x - r, p_max.y - r), uv_br, col); + PrimWriteVtx(ImVec2(p_max.x, p_max.y - r), uv_bl, col); + PrimWriteVtx(ImVec2(p_max.x, p_max.y), uv_tl, col); + PrimWriteVtx(ImVec2(p_max.x - r, p_max.y), uv_tr, col); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+1); PrimWriteIdx(idx+2); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+2); PrimWriteIdx(idx+3); + } + + if (flags & ImDrawFlags_RoundCornersBottomLeft) + { + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + PrimWriteVtx(ImVec2(p_min.x, p_max.y - r), uv_bl, col); + PrimWriteVtx(ImVec2(p_min.x + r, p_max.y - r), uv_br, col); + PrimWriteVtx(ImVec2(p_min.x + r, p_max.y), uv_tr, col); + PrimWriteVtx(ImVec2(p_min.x, p_max.y), uv_tl, col); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+1); PrimWriteIdx(idx+2); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+2); PrimWriteIdx(idx+3); + } + + const float r_tl = (flags & ImDrawFlags_RoundCornersTopLeft) ? r : 0; + const float r_tr = (flags & ImDrawFlags_RoundCornersTopRight) ? r : 0; + const float r_br = (flags & ImDrawFlags_RoundCornersBottomRight) ? r : 0; + const float r_bl = (flags & ImDrawFlags_RoundCornersBottomLeft) ? r : 0; + const ImVec2 opaque_uv = _Data->TexUvWhitePixel; + + { + // Top + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + const float left = p_min.x + r_tl; + const float right = p_max.x - r_tr; + PrimWriteVtx(ImVec2(left, p_min.y), opaque_uv, col); + PrimWriteVtx(ImVec2(right, p_min.y), opaque_uv, col); + PrimWriteVtx(ImVec2(right, p_min.y + t), opaque_uv, col); + PrimWriteVtx(ImVec2(left, p_min.y + t), opaque_uv, col); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+1); PrimWriteIdx(idx+2); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+2); PrimWriteIdx(idx+3); + } + { + // Left + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + const float top = p_min.y + ImMax(r_tl, t); + const float bottom = p_max.y - ImMax(r_bl, t); + PrimWriteVtx(ImVec2(p_min.x, top), opaque_uv, col); + PrimWriteVtx(ImVec2(p_min.x + t, top), opaque_uv, col); + PrimWriteVtx(ImVec2(p_min.x + t, bottom), opaque_uv, col); + PrimWriteVtx(ImVec2(p_min.x, bottom), opaque_uv, col); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+1); PrimWriteIdx(idx+2); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+2); PrimWriteIdx(idx+3); + } + { + // Right + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + const float top = p_min.y + ImMax(r_tr, t); + const float bottom = p_max.y - ImMax(r_br, t); + PrimWriteVtx(ImVec2(p_max.x - t, top), opaque_uv, col); + PrimWriteVtx(ImVec2(p_max.x, top), opaque_uv, col); + PrimWriteVtx(ImVec2(p_max.x, bottom), opaque_uv, col); + PrimWriteVtx(ImVec2(p_max.x - t, bottom), opaque_uv, col); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+1); PrimWriteIdx(idx+2); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+2); PrimWriteIdx(idx+3); + } + { + // Bottom + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + const float left = p_min.x + r_bl; + const float right = p_max.x - r_br; + PrimWriteVtx(ImVec2(left, p_max.y - t), opaque_uv, col); + PrimWriteVtx(ImVec2(right, p_max.y - t), opaque_uv, col); + PrimWriteVtx(ImVec2(right, p_max.y), opaque_uv, col); + PrimWriteVtx(ImVec2(left, p_max.y), opaque_uv, col); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+1); PrimWriteIdx(idx+2); + PrimWriteIdx(idx+0); PrimWriteIdx(idx+2); PrimWriteIdx(idx+3); + } + + const int idx_used = (int)(_IdxWritePtr - start_idx_ptr); + const int vtx_used = (int)(_VtxWritePtr - start_vtx_ptr); + if (idx_used < idx_allocated || vtx_used < vtx_allocated) + PrimUnreserve(idx_allocated - idx_used, vtx_allocated - vtx_used); +} + // p_min = upper-left, p_max = lower-right // Note we don't render 1 pixels sized rectangles properly. void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, float thickness, ImDrawFlags flags) @@ -1592,8 +1711,49 @@ void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, fl return; } + ImDrawFlags stroke_pos = (flags & ImDrawFlags_StrokeMask_); + if ((stroke_pos == ImDrawFlags_StrokeInside || stroke_pos == ImDrawFlags_StrokeOutside) && ImIsTruncated4(p_min.x, p_min.y, p_max.x, p_min.y) && ImIsTruncated(rounding) && ImIsTruncated(thickness)) + { + if ((flags & ImDrawFlags_RoundCornersMask_) == 0) + flags |= ImDrawFlags_RoundCornersAll; + + rounding = ImMin(rounding, ImFabs(p_max.x - p_min.x) * (((flags & ImDrawFlags_RoundCornersTop) == ImDrawFlags_RoundCornersTop) || ((flags & ImDrawFlags_RoundCornersBottom) == ImDrawFlags_RoundCornersBottom) ? 0.5f : 1.0f) - 1.0f); + rounding = ImMin(rounding, ImFabs(p_max.y - p_min.y) * (((flags & ImDrawFlags_RoundCornersLeft) == ImDrawFlags_RoundCornersLeft) || ((flags & ImDrawFlags_RoundCornersRight) == ImDrawFlags_RoundCornersRight) ? 0.5f : 1.0f) - 1.0f); + + int s_rounding = (int)rounding; + int s_thickness = (int)thickness; + + ImVec2 s_min = p_min; + ImVec2 s_max = p_max; + if (stroke_pos == ImDrawFlags_StrokeOutside) + { + s_min -= ImVec2(thickness, thickness); + s_max += ImVec2(thickness, thickness); + s_rounding += s_thickness; + } + + if (s_rounding <= 0 || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) + { + const float t = (float)s_thickness; + PrimReserve(6*4, 4*4); + PrimRect(ImVec2(p_min.x, p_min.y), ImVec2(p_max.x, p_min.y + t), col); + PrimRect(ImVec2(p_min.x, p_min.y + t), ImVec2(p_min.x + t, p_max.y - t), col); + PrimRect(ImVec2(p_max.x - t, p_min.y + t), ImVec2(p_max.x, p_max.y - t), col); + PrimRect(ImVec2(p_min.x, p_max.y - t), ImVec2(p_max.x, p_max.y), col); + return; + } + if (s_thickness <= IM_DRAWLIST_TEX_CORNERS_THICKNESS_MAX && s_rounding <= IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX) + { + const int size = ImMax(s_rounding, s_thickness); + const int idx = (s_thickness - 1) * IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX + s_rounding - 1; + IM_ASSERT(idx >= 0 && idx < IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX * IM_DRAWLIST_TEX_CORNERS_THICKNESS_MAX); + const ImVec4 tex_uvs = _Data->TexUvCornerStrokes[idx]; + _AddRectBaked(s_min, s_max, col, (float)size, (float)thickness, tex_uvs, flags); + return; + } + } + const ImVec2 offset(thickness * 0.5f, thickness * 0.5f); - const ImDrawFlags stroke_pos = (flags & ImDrawFlags_StrokeMask_); if (stroke_pos == ImDrawFlags_StrokeInside) PathRect(p_min + offset, p_max - offset, rounding - thickness * 0.5f, flags); else if (stroke_pos == ImDrawFlags_StrokeCenter) @@ -1610,7 +1770,7 @@ void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, fl PathStroke(col, thickness, ImDrawFlags_Closed); } -void ImDrawList::_AddMirrored9Slice(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float r, ImVec4 tex_uvs, ImDrawFlags flags) +void ImDrawList::_AddRectFilledBaked(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float r, ImVec4 tex_uvs, ImDrawFlags flags) { // 9-slice with corner mirroring const ImVec2 uv_tl(tex_uvs.x, tex_uvs.y); @@ -1681,7 +1841,7 @@ void ImDrawList::AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 c else if (s_rounding <= IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX) { ImVec4 tex_uvs = _Data->TexUvCornerFills[s_rounding - 1]; - _AddMirrored9Slice(p_min, p_max, col, ImMax(2.0f, (float)s_rounding), tex_uvs, flags); + _AddRectFilledBaked(p_min, p_max, col, ImMax(2.0f, (float)s_rounding), tex_uvs, flags); } else { @@ -3840,6 +4000,17 @@ static ImU8 SampleCorner(float x, float y, float cx, float cy, float r) return (ImU8)(ImClamp(1.0f - (d - r + aa_width * 0.5f) / aa_width, 0.0f, 1.0f) * 255.0f); } +static ImU8 SampleCornerStroke(float x, float y, float cx, float cy, float outer_r, float inner_r) +{ + const float dx = ImMin(x, cx) - cx; + const float dy = ImMin(y, cy) - cy; + const float d = ImSqrt(dx*dx + dy*dy); + const float aa_width = 1.0f; + const float outer = 1.0f - (d - outer_r + aa_width * 0.5f) / aa_width; + const float inner = (d - inner_r + aa_width * 0.5f) / aa_width; + return (ImU8)(ImClamp(ImMin(inner, outer), 0.0f, 1.0f) * 255.f); +} + static void ImFontAtlasBuildUpdateTexDataCorners(ImFontAtlas* atlas) { // Pack and store identifier so we can refresh UV coordinates on texture resize. @@ -3851,10 +4022,30 @@ static void ImFontAtlasBuildUpdateTexDataCorners(ImFontAtlas* atlas) if (add_and_draw) { ImVec2i pack_size(0, 0); + // Filled corners + int row_width = 0; + int row_height = 0; for (int i = 0; i < IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX; i++) { - pack_size.x += ImMax(2, i+1) + 2; - pack_size.y = ImMax(pack_size.y, (i+1) + 2); + const int size = ImMax(2, i+1) + 2; + row_width += size; + row_height = ImMax(row_height, size); + } + pack_size.x = ImMax(pack_size.x, row_width); + pack_size.y += row_height; + // Rounded corners + for (int t = 0; t < IM_DRAWLIST_TEX_CORNERS_THICKNESS_MAX; t++) + { + row_width = 0; + row_height = 0; + for (int i = 0; i < IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX; i++) + { + const int size = ImMax(2, ImMax(i+1, t+1)) + 2; + row_width += size; + row_height = ImMax(row_height, size); + } + pack_size.x = ImMax(pack_size.x, row_width); + pack_size.y += row_height; } builder->PackIdCornersTexData = atlas->AddCustomRect(pack_size.x, pack_size.y, &r); IM_ASSERT(builder->PackIdCornersTexData != ImFontAtlasRectId_Invalid); @@ -3862,16 +4053,20 @@ static void ImFontAtlasBuildUpdateTexDataCorners(ImFontAtlas* atlas) int x = r.x; int y = r.y; - for (int n = 0; n < IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX; n++) // +1 because of the zero-width row + int row_height = 0; + + // Filled + for (int n = 0; n < IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX; n++) { - const int s = ImMax(2, n+1); // We need at least 2px so that stretching has solid color. + const int rounding = n+1; + const int s = ImMax(2, rounding); // We need at least 2px so that stretching has solid color. const int w = s + 2; const int h = s + 2; // Corner circle const float cx = 1.0f + (float)(n+1); const float cy = 1.0f + (float)(n+1); - const float rad = (float)(n+1); + const float rad = (float)rounding; IM_ASSERT((x + w) <= (r.x + r.w) && (y + h) <= (r.y + r.h)); // Make sure we're inside the texture bounds before we start writing pixels // Write each slice @@ -3904,6 +4099,64 @@ static void ImFontAtlasBuildUpdateTexDataCorners(ImFontAtlas* atlas) atlas->TexUvCornerFills[n] = ImVec4(uv0.x, uv0.y, uv1.x, uv1.y); x += w; + row_height = ImMax(row_height, h); + } + y += row_height; + + // Stroked + for (int t = 0; t < IM_DRAWLIST_TEX_CORNERS_THICKNESS_MAX; t++) + { + x = r.x; + row_height = 0; + for (int n = 0; n < IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX; n++) + { + const int rounding = n+1; + const int thickness = t+1; + const int s = ImMax(rounding, thickness); + const int w = s + 2; + const int h = s + 2; + + // Corner circle + const float cx = 1.0f + (float)(n + 1); + const float cy = 1.0f + (float)(n + 1); + const float outer_rad = (float)rounding; + const float inner_rad = (float)(rounding - thickness); + + IM_ASSERT((x + w) <= (r.x + r.w) && (y + h) <= (r.y + r.h)); // Make sure we're inside the texture bounds before we start writing pixels + + // Write each slice + if (add_and_draw && tex->Format == ImTextureFormat_Alpha8) + { + ImU8* write_ptr = (ImU8*)tex->GetPixelsAt(x, y); + const int pitch = tex->Width; + for (int ly = 0; ly < h; ly++) + { + for (int lx = 0; lx < h; lx++) + write_ptr[lx] = SampleCornerStroke((float)lx + 0.5f, (float)ly + 0.5f, cx, cy, outer_rad, inner_rad); + write_ptr += pitch; + } + } + else if (add_and_draw && tex->Format == ImTextureFormat_RGBA32) + { + ImU32* write_ptr = (ImU32*)(void*)tex->GetPixelsAt(x, y); + const int pitch = tex->Width; + for (int ly = 0; ly < h; ly++) + { + for (int lx = 0; lx < h; lx++) + write_ptr[lx] = IM_COL32(255, 255, 255, SampleCornerStroke((float)lx + 0.5f, (float)ly + 0.5f, cx, cy, outer_rad, inner_rad)); + write_ptr += pitch; + } + } + + // Refresh UV coordinates + ImVec2 uv0 = ImVec2((float)(x + 1), (float)(y + 1)) * atlas->TexUvScale; + ImVec2 uv1 = ImVec2((float)(x + 1 + s), (float)(y + 1 + s)) * atlas->TexUvScale; + atlas->TexUvCornerStrokes[t*IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX + n] = ImVec4(uv0.x, uv0.y, uv1.x, uv1.y); + + x += w; + row_height = ImMax(row_height, h); + } + y += row_height; } } @@ -4343,6 +4596,7 @@ void ImFontAtlasUpdateDrawListsSharedData(ImFontAtlas* atlas) shared_data->TexUvWhitePixel = atlas->TexUvWhitePixel; shared_data->TexUvLines = atlas->TexUvLines; shared_data->TexUvCornerFills = atlas->TexUvCornerFills; + shared_data->TexUvCornerStrokes = atlas->TexUvCornerStrokes; } } diff --git a/imgui_internal.h b/imgui_internal.h index 92aa25c02..f89873135 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -920,6 +920,7 @@ struct IMGUI_API ImDrawListSharedData ImVec2 TexUvWhitePixel; // UV of white pixel in the atlas (== FontAtlas->TexUvWhitePixel) const ImVec4* TexUvLines; // UV of anti-aliased lines in the atlas (== FontAtlas->TexUvLines) const ImVec4* TexUvCornerFills; // UV of rounded corner (== FontAtlas->TexUvCornerFills) + const ImVec4* TexUvCornerStrokes; ImFontAtlas* FontAtlas; // Current font atlas ImFont* Font; // Current font (used for simplified AddText overload) float FontSize; // Current font size (used for for simplified AddText overload)