22
33#include < imgui.h>
44#include < imgui_internal.h>
5+ #include < limits>
56
67inline static void CopyIOEvents (ImGuiContext* src, ImGuiContext* dst, ImVec2 origin, float scale)
78{
89 dst->PlatformImeData = src->PlatformImeData ;
910 dst->IO .DeltaTime = src->IO .DeltaTime ;
11+
12+ // Intentionally copy InputEventsTrail (last frame's already-processed events)
13+ // rather than InputEventsQueue (this frame's pending events). Copying the queue
14+ // would cause every event to be processed twice — once by the outer context,
15+ // once here — resulting in duplicated inputs. The trade-off is exactly one
16+ // frame of input latency inside the inner context.
1017 dst->InputEventsQueue = src->InputEventsTrail ;
1118 for (ImGuiInputEvent& e : dst->InputEventsQueue ) {
1219 if (e.Type == ImGuiInputEventType_MousePos) {
@@ -16,40 +23,152 @@ inline static void CopyIOEvents(ImGuiContext* src, ImGuiContext* dst, ImVec2 ori
1623 }
1724}
1825
26+ // ---------------------------------------------------------------------------
27+ // AppendDrawData
28+ //
29+ // Blits one inner-context draw list into the outer window's draw list,
30+ // transforming vertex positions by (scale, origin) and adjusting all offsets
31+ // so the appended commands are valid in the outer buffer's index space.
32+ //
33+ // Must be called with the outer context active — ImGui::GetIO() inside this
34+ // function reads the OUTER context's BackendFlags, which is correct because
35+ // ContainedContext::end() restores the outer context before calling this.
36+ //
37+ // The caller (end()) pre-reserves VtxBuffer and IdxBuffer on the outer draw
38+ // list using draw_data->TotalVtxCount / TotalIdxCount before the loop, so
39+ // the resize() calls here will not trigger reallocs.
40+ //
41+ // _VtxCurrentIdx semantics (critical):
42+ // ImGui asserts _VtxCurrentIdx < (1<<16) after every primitive when using
43+ // 16-bit indices. This value tracks vertices in the CURRENT segment only
44+ // (since the last VtxOffset boundary), NOT the total outer buffer size.
45+ // It must be set to the segment-relative vertex count, not an absolute
46+ // outer-buffer position, which could exceed 65535.
47+ // ---------------------------------------------------------------------------
1948inline static void AppendDrawData (ImDrawList* src, ImVec2 origin, float scale)
2049{
21- // TODO optimize if vtx_start == 0 || if idx_start == 0
2250 ImDrawList* dl = ImGui::GetWindowDrawList ();
23- const int vtx_start = dl->VtxBuffer .size ();
24- const int idx_start = dl->IdxBuffer .size ();
25- dl->VtxBuffer .resize (dl->VtxBuffer .size () + src->VtxBuffer .size ());
26- dl->IdxBuffer .resize (dl->IdxBuffer .size () + src->IdxBuffer .size ());
27- dl->CmdBuffer .reserve (dl->CmdBuffer .size () + src->CmdBuffer .size ());
28- dl->_VtxWritePtr = dl->VtxBuffer .Data + vtx_start;
29- dl->_IdxWritePtr = dl->IdxBuffer .Data + idx_start;
30- const ImDrawVert* vtx_read = src->VtxBuffer .Data ;
31- const ImDrawIdx* idx_read = src->IdxBuffer .Data ;
32- for (int i = 0 , c = src->VtxBuffer .size (); i < c; ++i) {
33- dl->_VtxWritePtr [i].uv = vtx_read[i].uv ;
34- dl->_VtxWritePtr [i].col = vtx_read[i].col ;
35- dl->_VtxWritePtr [i].pos = vtx_read[i].pos * scale + origin;
51+
52+ // Early exit if buffers empty
53+ if (src->VtxBuffer .empty () || src->CmdBuffer .empty ()) {
54+ return ;
55+ }
56+
57+ const bool hasVtxOffset = (ImGui::GetIO ().BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset) != 0 ;
58+
59+ // Extend destination buffers and transform vertices into place.
60+ // VtxBuffer and IdxBuffer were pre-reserved in end() so these resize()
61+ // calls should not realloc in the common case.
62+ const unsigned int vtx_start = static_cast <unsigned int >(dl->VtxBuffer .Size );
63+ const unsigned int idx_start = static_cast <unsigned int >(dl->IdxBuffer .Size );
64+
65+ dl->VtxBuffer .resize (dl->VtxBuffer .Size + src->VtxBuffer .Size );
66+ dl->IdxBuffer .resize (dl->IdxBuffer .Size + src->IdxBuffer .Size );
67+ dl->CmdBuffer .reserve (dl->CmdBuffer .Size + src->CmdBuffer .Size );
68+
69+ {
70+ ImDrawVert* dst_v = dl->VtxBuffer .Data + vtx_start;
71+ const ImDrawVert* src_v = src->VtxBuffer .Data ;
72+ for (int i = 0 ; i < src->VtxBuffer .Size ; ++i) {
73+ dst_v[i].uv = src_v[i].uv ;
74+ dst_v[i].col = src_v[i].col ;
75+ dst_v[i].pos = src_v[i].pos * scale + origin;
76+ }
3677 }
37- for (int i = 0 , c = src->IdxBuffer .size (); i < c; ++i) {
38- dl->_IdxWritePtr [i] = idx_read[i] + vtx_start;
78+
79+ // Copy indices and fixup commands.
80+ ImDrawIdx* dst_idx_base = dl->IdxBuffer .Data + idx_start;
81+
82+ if (hasVtxOffset)
83+ {
84+ // Hot path: all modern backends (DX11/12, Vulkan, Metal, GL3+).
85+
86+ // Indices are segment-relative and require no per-index arithmetic —
87+ // bulk copy the entire index buffer in one shot, then fix up cmd
88+ // offsets in the command loop. This uses a single SIMD-optimised memcpy
89+ // instead of a scalar loop.
90+ memcpy (dst_idx_base, src->IdxBuffer .Data , src->IdxBuffer .Size * sizeof (ImDrawIdx));
91+
92+ // Cache for segment boundary scan: ImGui emits commands in non-decreasing
93+ // VtxOffset order, so consecutive commands often share the same segment.
94+ // Recomputing the forward scan per command would be O(n^2); caching the
95+ // result per unique VtxOffset keeps it O(n).
96+ unsigned int cached_vtx_offset = UINT_MAX ;
97+ unsigned int cached_seg_vtx_count = 0 ;
98+
99+ for (int ci = 0 ; ci < src->CmdBuffer .Size ; ++ci) {
100+ ImDrawCmd cmd = src->CmdBuffer [ci];
101+
102+ cmd.ClipRect .x = cmd.ClipRect .x * scale + origin.x ;
103+ cmd.ClipRect .y = cmd.ClipRect .y * scale + origin.y ;
104+ cmd.ClipRect .z = cmd.ClipRect .z * scale + origin.x ;
105+ cmd.ClipRect .w = cmd.ClipRect .w * scale + origin.y ;
106+
107+ // Compute the vertex count for this segment so _VtxCurrentIdx
108+ // stays segment-relative (never exceeds 65535 with 16-bit indices).
109+ // Skip the scan when this command shares a VtxOffset with the
110+ // previous one — same segment, boundary already known.
111+ if (cmd.VtxOffset != cached_vtx_offset) {
112+ cached_vtx_offset = cmd.VtxOffset ;
113+ unsigned int next_vtx_offset = static_cast <unsigned int >(src->VtxBuffer .Size );
114+ for (int ni = ci + 1 ; ni < src->CmdBuffer .Size ; ++ni) {
115+ if (src->CmdBuffer [ni].VtxOffset > cmd.VtxOffset ) {
116+ next_vtx_offset = src->CmdBuffer [ni].VtxOffset ;
117+ break ;
118+ }
119+ }
120+ cached_seg_vtx_count = next_vtx_offset - cmd.VtxOffset ;
121+ }
122+
123+ // Segment-relative count keeps the ImGui 16-bit index assert happy.
124+ dl->_VtxCurrentIdx = cached_seg_vtx_count;
125+
126+ cmd.VtxOffset += vtx_start;
127+ cmd.IdxOffset += idx_start;
128+ dl->CmdBuffer .push_back (cmd);
129+ }
39130 }
40- for (auto cmd : src->CmdBuffer ) {
41- cmd.IdxOffset += idx_start;
42- IM_ASSERT (cmd.VtxOffset == 0 );
43- cmd.ClipRect .x = cmd.ClipRect .x * scale + origin.x ;
44- cmd.ClipRect .y = cmd.ClipRect .y * scale + origin.y ;
45- cmd.ClipRect .z = cmd.ClipRect .z * scale + origin.x ;
46- cmd.ClipRect .w = cmd.ClipRect .w * scale + origin.y ;
47- dl->CmdBuffer .push_back (cmd);
131+ else {
132+ // Cold path: Legacy backends without RendererHasVtxOffset (OpenGL 2.x / ES2).
133+
134+ // Bake the vertex offset into each index to produce absolute outer-buffer
135+ // indices, since these backends cannot use cmd.VtxOffset to shift the base.
136+ const ImDrawIdx* src_idx_base = src->IdxBuffer .Data ;
137+
138+ for (auto cmd : src->CmdBuffer ) { // Note: cmd is a local copy
139+ IM_ASSERT (cmd.VtxOffset == 0 && " Non-zero VtxOffset in legacy path; backend flag mismatch. Should not happen." );
140+
141+ // Adjust clipping
142+ cmd.ClipRect .x = cmd.ClipRect .x * scale + origin.x ;
143+ cmd.ClipRect .y = cmd.ClipRect .y * scale + origin.y ;
144+ cmd.ClipRect .z = cmd.ClipRect .z * scale + origin.x ;
145+ cmd.ClipRect .w = cmd.ClipRect .w * scale + origin.y ;
146+
147+ const unsigned int base = vtx_start + cmd.VtxOffset ;
148+ // Verify the baked indices will fit in ImDrawIdx, handles both 16 and 32-bit indices.
149+ IM_ASSERT ( (sizeof (ImDrawIdx) >= 4 ||
150+ base + static_cast <unsigned int >(src->VtxBuffer .Size ) - 1u
151+ <= static_cast <unsigned int >(std::numeric_limits<ImDrawIdx>::max ()))
152+ && " Vertex count exceeds ImDrawIdx range; enable RendererHasVtxOffset or use 32-bit indices" );
153+
154+ const ImDrawIdx* si = src_idx_base + cmd.IdxOffset ;
155+ ImDrawIdx* di = dst_idx_base + cmd.IdxOffset ;
156+ for (unsigned int ii = 0 ; ii < cmd.ElemCount ; ++ii) {
157+ di[ii] = static_cast <ImDrawIdx>(si[ii] + base);
158+ }
159+ cmd.VtxOffset = 0 ;
160+ cmd.IdxOffset += idx_start;
161+ dl->CmdBuffer .push_back (cmd);
162+ }
163+
164+ // Guaranteed safe by the IM_ASSERT above.
165+ dl->_VtxCurrentIdx = vtx_start + static_cast <unsigned int >(src->VtxBuffer .Size );
48166 }
49167
50- dl->_VtxCurrentIdx += src->VtxBuffer .size ();
51- dl->_VtxWritePtr = dl->VtxBuffer .Data + dl->VtxBuffer .size ();
52- dl->_IdxWritePtr = dl->IdxBuffer .Data + dl->IdxBuffer .size ();
168+ // Advance write pointers to the new buffer ends.
169+ // _VtxCurrentIdx was already set inside each path above.
170+ dl->_VtxWritePtr = dl->VtxBuffer .Data + dl->VtxBuffer .Size ;
171+ dl->_IdxWritePtr = dl->IdxBuffer .Data + dl->IdxBuffer .Size ;
53172}
54173
55174struct ContainedContextConfig
@@ -104,7 +223,10 @@ inline ContainedContext::~ContainedContext()
104223 if (m_ctx) ImGui::DestroyContext (m_ctx);
105224}
106225
107- // Call after Begin()
226+ // Targets whichever context is current at call time.
227+ // In begin(), this is called twice: once for the outer context's child window
228+ // (so the outer renderer rasterizes at the correct density), and once inside
229+ // the inner context's Begin() when extra_window_wrapper is enabled.
108230inline void ContainedContext::setFontDensity ()
109231{
110232#if IMGUI_VERSION_NUM >= 19198
@@ -117,6 +239,8 @@ inline void ContainedContext::begin()
117239 ImGui::PushID (this );
118240 ImGui::PushStyleColor (ImGuiCol_ChildBg, m_config.color );
119241 ImGui::BeginChild (" view_port" , m_config.size , 0 , ImGuiWindowFlags_NoMove);
242+ // Set font density on the OUTER context's child window so the outer renderer
243+ // rasterizes fonts at the correct scale before we switch context below.
120244 setFontDensity ();
121245 ImGui::PopStyleColor ();
122246 m_pos = ImGui::GetWindowPos ();
@@ -135,11 +259,15 @@ inline void ContainedContext::begin()
135259 ImGui::GetIO ().DisplaySize = m_size / m_scale;
136260 ImGui::GetIO ().ConfigInputTrickleEventQueue = false ;
137261
138- // Copy the ImGuiBackendFlags_RendererHasTextures flag as they need to be matching.
139- // This will also copy the ImGuiBackendFlags_RendererHasVtxOffset flag which will be more optimal in case large draw calls are being made.
140- ImGui::GetIO ().ConfigFlags = m_original_ctx->IO .ConfigFlags ;
262+ // Copy backend flags so the inner context matches the outer renderer's
263+ // capabilities. This includes RendererHasVtxOffset (enables the optimised
264+ // AppendDrawData path) and RendererHasTextures (must match for texture IDs
265+ // to be interpreted correctly).
266+ ImGui::GetIO ().ConfigFlags = m_original_ctx->IO .ConfigFlags ;
141267 ImGui::GetIO ().BackendFlags = m_original_ctx->IO .BackendFlags ;
142268#ifdef IMGUI_HAS_VIEWPORT
269+ // Viewport and docking features require the platform backend to cooperate;
270+ // strip them from the inner context which has no platform window of its own.
143271 ImGui::GetIO ().ConfigFlags &= ~(ImGuiConfigFlags_ViewportsEnable | ImGuiConfigFlags_DockingEnable);
144272#endif
145273
@@ -152,6 +280,7 @@ inline void ContainedContext::begin()
152280 ImGui::PushStyleVar (ImGuiStyleVar_WindowPadding, ImVec2 (0 , 0 ));
153281 ImGui::Begin (" viewport_container" , nullptr , ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoMove
154282 | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
283+ // Set font density again now inside the inner context.
155284 setFontDensity ();
156285 ImGui::PopStyleVar ();
157286}
@@ -175,6 +304,17 @@ inline void ContainedContext::end()
175304 ImGui::SetCurrentContext (m_original_ctx);
176305 m_original_ctx = nullptr ;
177306
307+ // Pre-reserve outer draw list buffers using the total counts from the inner
308+ // draw data. This prevents repeated reallocs inside AppendDrawData when
309+ // there are multiple CmdLists to blit.
310+ {
311+ ImDrawList* dl = ImGui::GetWindowDrawList ();
312+ dl->VtxBuffer .reserve (dl->VtxBuffer .Size + draw_data->TotalVtxCount );
313+ dl->IdxBuffer .reserve (dl->IdxBuffer .Size + draw_data->TotalIdxCount );
314+ }
315+
316+ // AppendDrawData is called with the outer context active, so ImGui::GetIO()
317+ // inside it correctly reads the outer context's BackendFlags.
178318 for (int i = 0 ; i < draw_data->CmdListsCount ; ++i)
179319 AppendDrawData (draw_data->CmdLists [i], m_origin, m_scale);
180320
@@ -193,7 +333,10 @@ inline void ContainedContext::end()
193333 m_scale = m_scaleTarget;
194334 }
195335 }
196- if (abs (m_scaleTarget - m_scale) >= 0 .015f / m_config.zoom_smoothness )
336+ // Guard against zoom_smoothness == 0: dividing by zero yields +inf, making
337+ // the threshold comparison always false — correct by accident but fragile.
338+ if (m_config.zoom_smoothness > 0 .f &&
339+ abs (m_scaleTarget - m_scale) >= 0 .015f / m_config.zoom_smoothness )
197340 {
198341 float cs = (m_scaleTarget - m_scale) / m_config.zoom_smoothness ;
199342 m_scroll += (ImGui::GetMousePos () - m_pos) / (m_scale + cs) - (ImGui::GetMousePos () - m_pos) / m_scale;
@@ -221,6 +364,9 @@ inline void ContainedContext::end()
221364 {
222365 m_scroll += ImGui::GetIO ().MouseDelta / m_scale;
223366 }
367+
368+ // Update inner context MousePos for the NEXT frame's input. ImGui reads
369+ // MousePos at NewFrame(), so writing it here (end of this frame) is correct.
224370 this ->m_ctx ->IO .MousePos = (ImGui::GetMousePos () - m_origin) / m_scale;
225371 ImGui::EndChild ();
226372 ImGui::PopID ();
0 commit comments