Skip to content

Commit 0d04439

Browse files
StArraySharpCopilot
andcommitted
fix: repair the MemoryEditor port and release 1.93.0.4
The pure C# port of imgui_club's MemoryEditor had three defects that the sample exposed as soon as the Memory tab was opened. - 0xC0000005 in Button("Options", null): the extern takes `const ImVec2&` as a nullable `ImVec2*`, so passing null made Dear ImGui dereference a null pointer. ImGui::Button() in C++ relies on a default argument; the managed port has to pass the zero vector explicitly. Same for BeginChild("##scrolling", null, ...). - The hex column rendered the literal text "X2" for every byte. `b.ToString("X2 ")` is not a standard format string: the trailing space turns it into a custom format where X and 2 are literals. The separator is now appended after formatting. - 0xC0000005 in InputText: the flags requested CallbackAlways, but no callback was passed, and Dear ImGui asserts the pointer is non-null before invoking it. The InputTextUserData callback from the original is now ported, including the CurrentBufOverwrite refresh and the CursorPos >= 2 completion rule. - The scrolling child used -float.MinValue, which is -FLT_MAX, where the original passes -FLT_MIN (the smallest positive normal). BeginChild clamped the child to 4 pixels, so the data area rendered empty. - Hex formatting follows OptUpperCaseHex instead of always upper case. Also in this release: - Glut.MouseWheelFunc is [Obsolete]: imgui_impl_glut.cpp compiles the body only under __FREEGLUT_EXT_H__, which Apple's GLUT framework does not define, so the macOS dylibs export 11 of the 12 declared entry points. - ImDrawDataSnapshot.CompactUnused reuses one key buffer instead of allocating a uint[] on every NewFrame. - docs/TODO.md is development scratch and therefore ignored. LibraryRevision is now 4. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7e55b03 commit 0d04439

7 files changed

Lines changed: 151 additions & 33 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ __pycache__/
3131
*.pyc
3232

3333
docs/dependency-tree.md
34+
docs/TODO.md
3435

3536
# NuGet packages produced by dotnet pack
3637
artifacts/

‎docs/CHANGELOG.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ Drops `net8.0`, adds macOS support, and removes `ImGui4NET.Backend.Null`.
2626
architectures.
2727
- Backends for macOS: `OpenGL2`, `OpenGL3`, `Glut`, `Metal`, `Metal4` and `OSX`.
2828
GLFW, SDL2/3 and Vulkan need headers that are not part of the SDK.
29+
- **Fix: `MemoryEditor` crashed with `0xC0000005`.** `Button("Options", null)`
30+
and `BeginChild("##scrolling", null, ...)` passed a null pointer to a
31+
`const ImVec2&` parameter, which Dear ImGui dereferences unconditionally. Both
32+
now pass a zeroed `ImVec2`, matching what `ImGui::Button(label)` does in C++.
33+
C++ references are generated as nullable pointers, so the compiler could not
34+
catch this; a by-reference parameter would be `in ImVec2`/`ref ImVec2` instead.
35+
- `Glut.MouseWheelFunc` is marked `[Obsolete]` with an explanation. Dear ImGui
36+
declares it unconditionally in `imgui_impl_glut.h` but compiles the body only
37+
under `__FREEGLUT_EXT_H__`, which Apple's GLUT framework does not define, so
38+
the macOS dylibs export 11 of the 12 declared entry points. One binding
39+
assembly serves every RID, so the method stays — calling it on macOS throws
40+
`EntryPointNotFoundException`, and the attribute makes that a compile-time
41+
warning at the call site instead of a runtime surprise.
2942
- **New package `ImGui4NET.Backend.Metal4`.** Metal 4 renderer backend for
3043
`osx-arm64` only. Unlike every other backend it records `minos=26.0`
3144
(`-mmacosx-version-min=26.0`), because `imgui_impl_metal4.mm` binds
@@ -58,6 +71,9 @@ Drops `net8.0`, adds macOS support, and removes `ImGui4NET.Backend.Null`.
5871
- `ImTextureQueue`'s per-texture records stay in native memory: their address
5972
is stored in `ImTextureData.QueueUserData` and dereferenced by the render
6073
thread.
74+
- `ImDrawDataSnapshot` reuses one key buffer instead of allocating a new
75+
`uint[]` on every `NewFrame`, so the steady-state snapshot path is
76+
allocation-free.
6177
- `Metal` and `OSX` are Objective-C++. Their bindings come from
6278
`tools/gen-objc-backend.sh`, which parses with `-x objective-c++` and hands the
6379
header to `postprocess.py --macho-symbols`: a Mach-O symbol table stores every

‎samples/ImGui4NET.Sample.OpenGL3/Screenshot.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Drawing;
22
using System.Drawing.Imaging;
33
using System.Runtime.InteropServices;
4+
using System.Runtime.Versioning;
45
using OpenTK.Graphics.OpenGL4;
56
using GlPixelFormat = OpenTK.Graphics.OpenGL4.PixelFormat;
67

@@ -11,6 +12,7 @@ namespace ImGui4NET.Sample.OpenGL3;
1112
/// inspected without a live window. Press F12, or pass a path on the command
1213
/// line to grab a frame automatically.
1314
/// </summary>
15+
[SupportedOSPlatform("windows")]
1416
internal static class Screenshot
1517
{
1618
/// <summary>Reads the current back buffer and writes it as a PNG.</summary>

‎src/Directory.Build.props‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
-->
3434
<PropertyGroup>
3535
<ImGuiVersion Condition="'$(ImGuiVersion)' == ''">1.93.0</ImGuiVersion>
36-
<LibraryRevision Condition="'$(LibraryRevision)' == ''">3</LibraryRevision>
36+
<LibraryRevision Condition="'$(LibraryRevision)' == ''">4</LibraryRevision>
3737

3838
<Version>$(ImGuiVersion).$(LibraryRevision)</Version>
3939
<AssemblyVersion>$(Version)</AssemblyVersion>

‎src/ImGui4NET.Backend.Glut/Generated/methods.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ public static partial class Methods
3333
[DllImport("imgui_backend_glut", CallingConvention = CallingConvention.Cdecl, EntryPoint = "_Z24ImGui_ImplGLUT_MouseFunciiii", ExactSpelling = true)]
3434
public static extern void MouseFunc(int button, int state, int x, int y);
3535

36+
/// <remarks>
37+
/// Unavailable on macOS: <c>imgui_impl_glut.cpp</c> compiles the body
38+
/// only under <c>__FREEGLUT_EXT_H__</c>, which Apple's GLUT framework
39+
/// does not define, so the symbol is missing from the macOS dylib.
40+
/// Calling this there throws <see cref="EntryPointNotFoundException"/>.
41+
/// </remarks>
42+
[Obsolete("MouseWheelFunc is not available on macOS.")]
3643
[DllImport("imgui_backend_glut", CallingConvention = CallingConvention.Cdecl, EntryPoint = "_Z29ImGui_ImplGLUT_MouseWheelFunciiii", ExactSpelling = true)]
3744
public static extern void MouseWheelFunc(int button, int dir, int x, int y);
3845

‎src/ImGui4NET.Club/ImDrawDataSnapshot.cs‎

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ private sealed class Entry
4545

4646
private readonly Dictionary<uint, Entry> _cache = [];
4747

48+
// Scratch buffer reused by CompactUnused so a steady-state snapshot does not
49+
// allocate a fresh key array every frame.
50+
private uint[] _compactKeys = [];
51+
4852
/// <summary>
4953
/// Releases every copied draw list and clears the snapshot.
5054
/// </summary>
@@ -123,21 +127,34 @@ private void CompactUnused(double currentTime)
123127
{
124128
double threshold = currentTime - MemoryCompactTimer;
125129

126-
uint[] keys = new uint[_cache.Count];
127-
_cache.Keys.CopyTo(keys, 0);
130+
int count = _cache.Count;
131+
if (count == 0)
132+
{
133+
return;
134+
}
128135

129-
foreach (uint key in keys)
130-
{
131-
Entry entry = _cache[key];
132-
if (entry.LastUsedTime > threshold)
136+
uint[] keys = _compactKeys;
137+
if (keys.Length < count)
133138
{
134-
continue;
139+
keys = new uint[count * 2];
140+
_compactKeys = keys;
135141
}
136142

137-
FreeDrawList(entry.OurCopy);
138-
_cache.Remove(key);
143+
_cache.Keys.CopyTo(keys, 0);
144+
145+
for (int index = 0; index < count; index++)
146+
{
147+
uint key = keys[index];
148+
Entry entry = _cache[key];
149+
if (entry.LastUsedTime > threshold)
150+
{
151+
continue;
152+
}
153+
154+
FreeDrawList(entry.OurCopy);
155+
_cache.Remove(key);
156+
}
139157
}
140-
}
141158

142159
/// <summary>Mirror of <c>ImDrawDataSnapshot::GetDrawListID</c>: hash of the pointer.</summary>
143160
private static uint GetDrawListID(ImDrawList* srcList)

‎src/ImGui4NET.Club/MemoryEditor.cs‎

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,24 @@ public sealed unsafe class MemoryEditor : IDisposable
113113
private sbyte* _dataInputBuf;
114114
private sbyte* _addrInputBuf;
115115

116+
/// <summary>
117+
/// Per-call payload handed to <see cref="DataInputCallback"/>.
118+
/// </summary>
119+
/// <remarks>
120+
/// The address of an instance is passed as <c>user_data</c>, so it lives on
121+
/// the stack for the duration of one <c>InputText</c> call and must stay
122+
/// blittable.
123+
/// <para>
124+
/// <see cref="DataInputUserData.CurrentBufOverwrite"/> is <c>char[3]</c> in
125+
/// C++ and holds the two hex digits plus a terminator.
126+
/// </para>
127+
/// </remarks>
128+
private struct DataInputUserData
129+
{
130+
public fixed sbyte CurrentBufOverwrite[3];
131+
public int CursorPos;
132+
}
133+
116134
private bool _disposed;
117135

118136
// Scratch buffer for the data preview; needs no persistence.
@@ -310,7 +328,11 @@ public void DrawContents(void* memDataVoid, nuint memSize, nuint baseDisplayAddr
310328
+ ImGui.GetTextLineHeightWithSpacing() * 3f;
311329
}
312330

313-
ImVec2 scrollingSize = new() { x = -float.MinValue, y = -footerHeight };
331+
// C++ writes this as ImVec2(-FLT_MIN, -footer_height): -FLT_MIN is the
332+
// smallest positive normal negated, a sentinel meaning "fill the
333+
// remaining width". C#'s float.MinValue is -FLT_MAX instead, which
334+
// would make BeginChild clamp the width to 4 pixels and hide the data.
335+
ImVec2 scrollingSize = new() { x = -1.17549435e-38f, y = -footerHeight };
314336
int childFlags = (int)ImGuiChildFlags.ImGuiChildFlags_None;
315337
int childWindowFlags = (int)ImGuiWindowFlags.ImGuiWindowFlags_NoMove
316338
| (int)ImGuiWindowFlags.ImGuiWindowFlags_NoNav;
@@ -393,9 +415,10 @@ public void DrawContents(void* memDataVoid, nuint memSize, nuint baseDisplayAddr
393415
? ImGui.GetColorU32((int)ImGuiCol.ImGuiCol_TextDisabled)
394416
: colorText;
395417

396-
string addressFormat = OptUpperCaseHex ? "X" : "x";
418+
// .NET has no "%02X " equivalent: a trailing space turns "X2 " into a
419+
// custom format string that is emitted literally. The separator is
420+
// appended after formatting instead.
397421
string byteFormat = OptUpperCaseHex ? "X2" : "x2";
398-
string byteSpaceFormat = OptUpperCaseHex ? "X2 " : "x2 ";
399422

400423
MouseHovered = false;
401424
MouseHoveredAddr = 0;
@@ -406,11 +429,11 @@ public void DrawContents(void* memDataVoid, nuint memSize, nuint baseDisplayAddr
406429
{
407430
nuint addr = (nuint)lineI * (nuint)Cols;
408431

409-
DrawAddressLine(drawList, s, lineI, baseDisplayAddr, addr, colorText, addressFormat);
432+
DrawAddressLine(drawList, s, lineI, baseDisplayAddr, addr, colorText, OptUpperCaseHex);
410433

411434
addr = (nuint)lineI * (nuint)Cols;
412435
DrawHexCells(drawList, memData, memSize, s, ref addr, colorText, colorDisabled,
413-
byteFormat, byteSpaceFormat, previewDataTypeSize, ref dataNext, ref dataEditingAddrNext);
436+
byteFormat, previewDataTypeSize, ref dataNext, ref dataEditingAddrNext);
414437

415438
if (OptShowAscii)
416439
{
@@ -469,7 +492,8 @@ private void HandleGotoRequest(nuint memSize, ImVec2 availSize)
469492

470493
if (_gotoAddr < memSize)
471494
{
472-
ImGui.BeginChild("##scrolling", null, 0, 0);
495+
ImVec2 scrollingSize = new() { x = 0f, y = 0f };
496+
ImGui.BeginChild("##scrolling", &scrollingSize, 0, 0);
473497
ImGui.SetScrollY((_gotoAddr / (nuint)Cols) * ImGui.GetTextLineHeight() - availSize.y * 0.5f);
474498
ImGui.EndChild();
475499

@@ -543,18 +567,18 @@ private void HandleOptionsPopup(Sizes s, ImVec2 contentsPosStart, ImVec2 content
543567
// -----------------------------------------------------------------------
544568

545569
private void DrawAddressLine(ImDrawList* drawList, Sizes s, int lineI, nuint baseDisplayAddr,
546-
nuint addr, uint colorText, string digitsFormat)
570+
nuint addr, uint colorText, bool upperCase)
547571
{
548572
_ = drawList;
549573
_ = colorText;
550574

551575
nuint displayAddr = baseDisplayAddr + addr;
552-
string text = FormatHex(displayAddr, s.AddrDigitsCount) + ": ";
576+
string text = FormatHex(displayAddr, s.AddrDigitsCount, OptUpperCaseHex) + ": ";
553577
ImGui.TextUnformatted(text);
554578
}
555579

556580
private void DrawHexCells(ImDrawList* drawList, byte* memData, nuint memSize, Sizes s,
557-
ref nuint addr, uint colorText, uint colorDisabled, string byteFormat, string byteSpaceFormat,
581+
ref nuint addr, uint colorText, uint colorDisabled, string byteFormat,
558582
nuint previewDataTypeSize, ref bool dataNext, ref nuint dataEditingAddrNext)
559583
{
560584
for (int n = 0; n < Cols && addr < memSize; n++, addr++)
@@ -617,13 +641,13 @@ private void DrawHexCells(ImDrawList* drawList, byte* memData, nuint memSize, Si
617641
}
618642
else
619643
{
620-
DrawStaticByte(memData, addr, colorDisabled, byteFormat, byteSpaceFormat, ref dataEditingAddrNext);
644+
DrawStaticByte(memData, addr, colorDisabled, byteFormat, ref dataEditingAddrNext);
621645
}
622646
}
623647
}
624648

625649
private void DrawStaticByte(byte* memData, nuint addr, uint colorDisabled,
626-
string byteFormat, string byteSpaceFormat, ref nuint dataEditingAddrNext)
650+
string byteFormat, ref nuint dataEditingAddrNext)
627651
{
628652
byte b = ReadFn != null ? ReadFn(memData, addr, UserData) : memData[addr];
629653

@@ -645,7 +669,7 @@ private void DrawStaticByte(byte* memData, nuint addr, uint colorDisabled,
645669
}
646670
else
647671
{
648-
ImGui.TextUnformatted(b.ToString(byteSpaceFormat));
672+
ImGui.TextUnformatted(b.ToString(byteFormat) + " ");
649673
}
650674
}
651675
else if (b == 0 && OptGreyOutZeroes)
@@ -656,7 +680,7 @@ private void DrawStaticByte(byte* memData, nuint addr, uint colorDisabled,
656680
}
657681
else
658682
{
659-
ImGui.TextUnformatted(b.ToString(byteSpaceFormat));
683+
ImGui.TextUnformatted(b.ToString(byteFormat) + " ");
660684
}
661685

662686
if (ImGui.IsItemHovered() != 0)
@@ -672,19 +696,58 @@ private void DrawStaticByte(byte* memData, nuint addr, uint colorDisabled,
672696
}
673697
}
674698

699+
// Mirrors ImGuiInputTextCallbackData's HasSelection(). Kept as a helper
700+
// because the generated struct exposes fields only.
701+
private static bool HasSelection(ImGuiInputTextCallbackData* data)
702+
=> data->SelectionStart != data->SelectionEnd;
703+
704+
/// <summary>
705+
/// Port of the <c>InputTextUserData::Callback</c> in the C++ original.
706+
/// </summary>
707+
/// <remarks>
708+
/// <c>InputText</c> owns the master copy of the text it edits, so when the
709+
/// user is not selecting anything the content is forced back to the byte
710+
/// currently in memory. Without this the widget would keep whatever was
711+
/// typed before the editor scrolled that address out of view.
712+
/// <para>
713+
/// The callback only runs because <c>CallbackAlways</c> is requested; Dear
714+
/// ImGui asserts the pointer is non-null in that case, so passing a flag
715+
/// without a callback aborts the process.
716+
/// </para>
717+
/// </remarks>
718+
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
719+
private static int DataInputCallback(ImGuiInputTextCallbackData* data)
720+
{
721+
DataInputUserData* userData = (DataInputUserData*)data->UserData;
722+
if (!HasSelection(data))
723+
{
724+
userData->CursorPos = data->CursorPos;
725+
}
726+
727+
if (data->SelectionStart == 0 && data->SelectionEnd == data->BufTextLen)
728+
{
729+
ImGuiInputTextCallbackData.DeleteChars(data, 0, data->BufTextLen);
730+
ImGuiInputTextCallbackData.InsertChars(data, 0, new string(userData->CurrentBufOverwrite));
731+
data->SelectionStart = 0;
732+
data->SelectionEnd = 2;
733+
data->CursorPos = 0;
734+
}
735+
736+
return 0;
737+
}
738+
675739
private void DrawEditingByte(ImDrawList* drawList, byte* memData, Sizes s, nuint addr,
676740
string byteFormat, ref bool dataNext, ref nuint dataEditingAddrNext)
677741
{
678742
_ = drawList;
679-
_ = s;
680743

681744
bool dataWrite = false;
682745
ImGui.PushID((void*)addr);
683746

684747
if (_dataEditingTakeFocus)
685748
{
686749
ImGui.SetKeyboardFocusHere(0);
687-
WriteCString(_addrInputBuf, InputBufSize, FormatHex(addr, s.AddrDigitsCount));
750+
WriteCString(_addrInputBuf, InputBufSize, FormatHex(addr, s.AddrDigitsCount, OptUpperCaseHex));
688751

689752
byte current = ReadFn != null ? ReadFn(memData, addr, UserData) : memData[addr];
690753
WriteCString(_dataInputBuf, InputBufSize, current.ToString(byteFormat));
@@ -704,7 +767,12 @@ private void DrawEditingByte(ImDrawList* drawList, byte* memData, Sizes s, nuint
704767

705768
ImGui.SetNextItemWidth(s.GlyphWidth * 2f);
706769

707-
if (ImGui.InputText("##data", _dataInputBuf, InputBufSize, flags) != 0)
770+
DataInputUserData userData = default;
771+
userData.CursorPos = -1;
772+
byte live = ReadFn != null ? ReadFn(memData, addr, UserData) : memData[addr];
773+
WriteCString(userData.CurrentBufOverwrite, 3, live.ToString(byteFormat));
774+
775+
if (ImGui.InputText("##data", _dataInputBuf, InputBufSize, flags, &DataInputCallback, &userData) != 0)
708776
{
709777
dataWrite = dataNext = true;
710778
}
@@ -720,6 +788,12 @@ private void DrawEditingByte(ImDrawList* drawList, byte* memData, Sizes s, nuint
720788
dataWrite = dataNext = false;
721789
}
722790

791+
// A cursor past the first digit means the user finished the byte.
792+
if (userData.CursorPos >= 2)
793+
{
794+
dataWrite = dataNext = true;
795+
}
796+
723797
if (!ReadOnly && dataWrite && TryParseHexByte(_dataInputBuf, out byte value))
724798
{
725799
if (WriteFn != null)
@@ -811,18 +885,19 @@ private void DrawOptionsLine(Sizes s, void* memData, nuint memSize, nuint baseDi
811885

812886
ImGuiStyle* style = ImGui.GetStyle();
813887
string rangeFormat = OptUpperCaseHex
814-
? $"Range {FormatHex(baseDisplayAddr, s.AddrDigitsCount)}.."
815-
: $"Range {FormatHex(baseDisplayAddr, s.AddrDigitsCount)}..";
888+
? $"Range {FormatHex(baseDisplayAddr, s.AddrDigitsCount, OptUpperCaseHex)}.."
889+
: $"Range {FormatHex(baseDisplayAddr, s.AddrDigitsCount, OptUpperCaseHex)}..";
816890

817-
if (ImGui.Button("Options", null) != 0)
891+
ImVec2 optionsButtonSize = new() { x = 0f, y = 0f };
892+
if (ImGui.Button("Options", &optionsButtonSize) != 0)
818893
{
819894
ImGui.OpenPopup("OptionsPopup");
820895
}
821896

822897
ImGui.SameLine(0f, -1f);
823898
_ = rangeFormat;
824899
ImGui.TextUnformatted(
825-
$"Range {FormatHex(baseDisplayAddr, s.AddrDigitsCount)}..{FormatHex(baseDisplayAddr + memSize - 1, s.AddrDigitsCount)}");
900+
$"Range {FormatHex(baseDisplayAddr, s.AddrDigitsCount, OptUpperCaseHex)}..{FormatHex(baseDisplayAddr + memSize - 1, s.AddrDigitsCount, OptUpperCaseHex)}");
826901

827902
ImGui.SameLine(0f, -1f);
828903
ImGui.SetNextItemWidth((s.AddrDigitsCount + 1) * s.GlyphWidth + style->FramePadding.x * 2f);
@@ -1055,8 +1130,8 @@ private static string FormatBinary(byte* buf, int width)
10551130
// Small helpers
10561131
// -----------------------------------------------------------------------
10571132

1058-
private static string FormatHex(nuint value, int digits)
1059-
=> value.ToString("X" + digits.ToString());
1133+
private static string FormatHex(nuint value, int digits, bool upper)
1134+
=> value.ToString((upper ? "X" : "x") + digits.ToString());
10601135

10611136
private static void WriteCString(sbyte* buffer, int size, string text)
10621137
{

0 commit comments

Comments
 (0)