Skip to content

Commit 652d2b6

Browse files
committed
Merge branch 'master' of https://github.com/Fattorino/ImNodeFlow
2 parents 1a0574d + 5fcf338 commit 652d2b6

2 files changed

Lines changed: 179 additions & 32 deletions

File tree

src/context_wrapper.h

Lines changed: 178 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@
22

33
#include <imgui.h>
44
#include <imgui_internal.h>
5+
#include <limits>
56

67
inline 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+
// ---------------------------------------------------------------------------
1948
inline 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

55174
struct 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.
108230
inline 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();

src/imgui_extra_math.inl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs)
3030
return lhs.x != rhs.x || lhs.y != rhs.y;
3131
}
3232

33+
# if IMGUI_VERSION_NUM < 19268
3334
inline ImVec2 operator*(const float lhs, const ImVec2& rhs)
3435
{
3536
return ImVec2(lhs * rhs.x, lhs * rhs.y);

0 commit comments

Comments
 (0)