Skip to content

Instantly share code, notes, and snippets.

@japajoe
Created January 29, 2025 15:56
Show Gist options
  • Save japajoe/558a86396d418a5bab8e63fee11fcc4f to your computer and use it in GitHub Desktop.
Save japajoe/558a86396d418a5bab8e63fee11fcc4f to your computer and use it in GitHub Desktop.
ImGui MemoryViewer in C#
using System;
using System.Runtime.InteropServices;
using ImGuiNET;
using System.Numerics;
public sealed unsafe class MemoryViewer
{
public bool Open = true;
public bool ReadOnly = false;
public int Cols = 16;
public bool OptShowOptions = true;
public bool OptShowDataPreview = true;
public bool OptShowHexII = false;
public bool OptShowAscii = true;
public bool OptGreyOutZeroes = true;
public bool OptUpperCaseHex = true;
public int OptMidColsCount = 8;
public int OptAddrDigitsCount = 0;
public float OptFooterExtraHeight = 0.0f;
public uint HighlightColor = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 1.0f, 1.0f, 0.2f));
public delegate byte ReadFnDelegate(IntPtr mem, ulong off, IntPtr userData);
public delegate void WriteFnDelegate(IntPtr mem, ulong off, byte d, IntPtr userData);
public delegate bool HighlightFnDelegate(IntPtr mem, ulong off, IntPtr userData);
public delegate uint BgColorFnDelegate(IntPtr mem, ulong off, IntPtr userData);
public ReadFnDelegate ReadFn = null;
public WriteFnDelegate WriteFn = null;
public HighlightFnDelegate HighlightFn = null;
public BgColorFnDelegate BgColorFn = null;
public IntPtr UserData = IntPtr.Zero;
public bool MouseHovered = false;
public ulong MouseHoveredAddr = 0;
private bool _contentsWidthChanged = false;
private ulong _dataPreviewAddr = ulong.MaxValue;
private ulong _dataEditingAddr = ulong.MaxValue;
private bool _dataEditingTakeFocus = false;
private byte[] _dataInputBuf = new byte[32];
private byte[] _addrInputBuf = new byte[32];
private ulong _gotoAddr = ulong.MaxValue;
private ulong _highlightMin = ulong.MaxValue;
private ulong _highlightMax = ulong.MaxValue;
private int _previewEndianness = 0;
private ImGuiDataType _previewDataType = ImGuiDataType.S32;
public void DrawWindow(string title, IntPtr memData, ulong memSize, ulong baseDisplayAddr = 0)
{
if (!Open) return;
ImGui.SetNextWindowSize(new Vector2(500, 300), ImGuiCond.FirstUseEver);
if (ImGui.Begin(title, ref Open))
{
DrawContents(memData, memSize, baseDisplayAddr);
}
ImGui.End();
}
private void DrawContents(IntPtr memData, ulong memSize, ulong baseDisplayAddr = 0)
{
if (Cols < 1) Cols = 1;
Sizes sizes = new Sizes();
CalcSizes(sizes, memSize, baseDisplayAddr);
if (OptShowOptions)
{
DrawOptionsLine(sizes, memData, memSize, baseDisplayAddr);
ImGui.Separator();
}
if (OptShowDataPreview)
{
//ImGui.Separator();
DrawPreviewLine(sizes, memData, memSize, baseDisplayAddr);
}
ImGui.BeginChild("##scrolling", new Vector2(0, -ImGui.GetFrameHeightWithSpacing() - OptFooterExtraHeight), ImGuiChildFlags.None, ImGuiWindowFlags.NoMove);
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(0, 0));
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0));
int lineTotalCount = (int)(((int)memSize + Cols - 1) / Cols);
ImGuiListClipperEx clipper = new ImGuiListClipperEx();
clipper.Begin(lineTotalCount, sizes.LineHeight);
while (clipper.Step())
{
for (int line_i = clipper.DisplayStart; line_i < clipper.DisplayEnd; line_i++)
{
ulong addr = (ulong)line_i * (ulong)Cols;
ImGui.Text($"{baseDisplayAddr + addr:X8}: ");
for (int n = 0; n < Cols && addr < memSize; n++, addr++)
{
float bytePosX = sizes.PosHexStart + sizes.HexCellWidth * n;
if (OptMidColsCount > 0)
bytePosX += (n / OptMidColsCount) * sizes.SpacingBetweenMidCols;
ImGui.SameLine(bytePosX);
byte b = ReadFn != null ? ReadFn(memData, addr, UserData) : Marshal.ReadByte(memData + (int)addr);
if (OptShowHexII)
{
if (b >= 32 && b < 128)
ImGui.Text($".{(char)b} ");
else if (b == 0xFF && OptGreyOutZeroes)
ImGui.TextDisabled("## ");
else if (b == 0x00)
ImGui.Text(" ");
else
ImGui.Text($"{b:X2} ");
}
else
{
if (b == 0 && OptGreyOutZeroes)
ImGui.TextDisabled("00 ");
else
ImGui.Text($"{b:X2} ");
}
if (ImGui.IsItemHovered())
{
MouseHovered = true;
MouseHoveredAddr = addr;
}
}
if (OptShowAscii)
{
//ImGui.SameLine(sizes.PosAsciiStart);
addr = (ulong)line_i * (ulong)Cols;
for (int n = 0; n < Cols && addr < memSize; n++, addr++)
{
ImGui.SameLine();
byte b = ReadFn != null ? ReadFn(memData, addr, UserData) : Marshal.ReadByte(memData + (int)addr);
char c = (b < 32 || b >= 128) ? '.' : (char)b;
ImGui.Text($"{c}");
//ImGui.SameLine();
}
}
}
}
ImGui.PopStyleVar(2);
ImGui.EndChild();
clipper.Dispose();
}
private void CalcSizes(Sizes sizes, ulong memSize, ulong baseDisplayAddr)
{
sizes.AddrDigitsCount = OptAddrDigitsCount;
if (sizes.AddrDigitsCount == 0)
{
for (ulong n = baseDisplayAddr + memSize - 1; n > 0; n >>= 4)
sizes.AddrDigitsCount++;
}
sizes.LineHeight = ImGui.GetTextLineHeight();
sizes.GlyphWidth = ImGui.CalcTextSize("F").X + 1;
sizes.HexCellWidth = (float)(int)(sizes.GlyphWidth * 2.5f);
sizes.SpacingBetweenMidCols = (float)(int)(sizes.HexCellWidth * 0.25f);
sizes.PosHexStart = (sizes.AddrDigitsCount + 2) * sizes.GlyphWidth;
sizes.PosHexEnd = sizes.PosHexStart + (sizes.HexCellWidth * Cols);
sizes.PosAsciiStart = sizes.PosHexEnd + sizes.GlyphWidth * 1;
sizes.PosAsciiEnd = sizes.PosAsciiStart + Cols * sizes.GlyphWidth;
sizes.WindowWidth = sizes.PosAsciiEnd + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().WindowPadding.X * 2 + sizes.GlyphWidth;
}
private void DrawOptionsLine(Sizes sizes, IntPtr memData, ulong memSize, ulong baseDisplayAddr)
{
ImGui.Text($"Range {baseDisplayAddr:X8}..{baseDisplayAddr + memSize - 1:X8}");
ImGui.SameLine();
ImGui.SetNextItemWidth(128);
if (ImGui.InputText("##addr", _addrInputBuf, (uint)_addrInputBuf.Length, ImGuiInputTextFlags.EnterReturnsTrue))
{
string address = System.Text.Encoding.ASCII.GetString(_addrInputBuf);
address = address.ToLower().Trim();
if(address.StartsWith("0x"))
{
address = address.Replace("0x", string.Empty);
if (ulong.TryParse(address, System.Globalization.NumberStyles.HexNumber, null, out ulong gotoAddr))
{
_gotoAddr = gotoAddr - baseDisplayAddr;
_highlightMin = _highlightMax = ulong.MaxValue;
}
}
else
{
if (ulong.TryParse(address, out ulong gotoAddr))
{
_gotoAddr = gotoAddr - baseDisplayAddr;
_highlightMin = _highlightMax = ulong.MaxValue;
}
}
}
if (_gotoAddr != ulong.MaxValue)
{
if (_gotoAddr < memSize)
{
ImGui.BeginChild("##scrolling");
ImGui.SetScrollFromPosY(ImGui.GetCursorStartPos().Y + (_gotoAddr / (ulong)Cols) * ImGui.GetTextLineHeight());
ImGui.EndChild();
_dataEditingAddr = _dataPreviewAddr = _gotoAddr;
_dataEditingTakeFocus = true;
}
_gotoAddr = ulong.MaxValue;
}
}
private void DrawPreviewLine(Sizes sizes, IntPtr memData, ulong memSize, ulong baseDisplayAddr)
{
ImGui.Text("Preview as:");
ImGui.SameLine();
ImGui.SetNextItemWidth(128);
if (ImGui.BeginCombo("##combo_type", DataTypeGetDesc(_previewDataType), ImGuiComboFlags.HeightLargest))
{
foreach (ImGuiDataType dataType in Enum.GetValues(typeof(ImGuiDataType)))
{
if (ImGui.Selectable(DataTypeGetDesc(dataType), _previewDataType == dataType))
_previewDataType = dataType;
}
ImGui.EndCombo();
}
ImGui.SameLine();
//ImGui.SetNextItemWidth(sizes.GlyphWidth * 6.0f + ImGui.GetStyle().FramePadding.X * 2.0f + ImGui.GetStyle().ItemInnerSpacing.X);
ImGui.SetNextItemWidth(128);
ImGui.Combo("##combo_endianness", ref _previewEndianness, "LE\0BE\0\0");
if (_dataPreviewAddr != ulong.MaxValue)
{
ValueUnion val = new ValueUnion();
byte[] buf = new byte[8];
ulong size = Math.Min((ulong)buf.Length, memSize - _dataPreviewAddr);
for (ulong i = 0; i < size; i++)
{
buf[i] = ReadFn != null ? ReadFn(memData, _dataPreviewAddr + i, UserData) : Marshal.ReadByte(memData + (int)(_dataPreviewAddr + i));
val.data[i] = buf[i];
}
switch(_previewDataType)
{
case ImGuiDataType.S8:
ImGui.Text("Dec: " + val.i8);
break;
case ImGuiDataType.U8:
ImGui.Text("Dec: " + val.u8);
break;
case ImGuiDataType.S16:
ImGui.Text("Dec: " + val.i16);
break;
case ImGuiDataType.U16:
ImGui.Text("Dec: " + val.u16);
break;
case ImGuiDataType.S32:
ImGui.Text("Dec: " + val.i32);
break;
case ImGuiDataType.U32:
ImGui.Text("Dec: " + val.u32);
break;
case ImGuiDataType.S64:
ImGui.Text("Dec: " + val.i64);
break;
case ImGuiDataType.U64:
ImGui.Text("Dec: " + val.u64);
break;
case ImGuiDataType.Float:
ImGui.Text("Dec: " + val.flt);
break;
case ImGuiDataType.Double:
ImGui.Text("Dec: " + val.dbl);
break;
case ImGuiDataType.Bool:
ImGui.Text("Dec: " + val.boolean);
break;
default:
ImGui.Text("Dec: " + BitConverter.ToInt32(buf, 0));
break;
}
//ImGui.Text("Dec: " + BitConverter.ToInt32(buf, 0));
ImGui.Text("Hex: " + BitConverter.ToString(buf).Replace("-", ""));
ImGui.Text("Bin: " + Convert.ToString(BitConverter.ToInt32(buf, 0), 2).PadLeft(32, '0'));
}
}
private string DataTypeGetDesc(ImGuiDataType dataType)
{
switch (dataType)
{
case ImGuiDataType.S8: return "Int8";
case ImGuiDataType.U8: return "Uint8";
case ImGuiDataType.S16: return "Int16";
case ImGuiDataType.U16: return "Uint16";
case ImGuiDataType.S32: return "Int32";
case ImGuiDataType.U32: return "Uint32";
case ImGuiDataType.S64: return "Int64";
case ImGuiDataType.U64: return "Uint64";
case ImGuiDataType.Float: return "Float";
case ImGuiDataType.Double: return "Double";
case ImGuiDataType.Bool: return "Bool";
default: return "Unknown";
}
}
private struct Sizes
{
public int AddrDigitsCount;
public float LineHeight;
public float GlyphWidth;
public float HexCellWidth;
public float SpacingBetweenMidCols;
public float PosHexStart;
public float PosHexEnd;
public float PosAsciiStart;
public float PosAsciiEnd;
public float WindowWidth;
}
[StructLayout(LayoutKind.Explicit, Size = 8)]
private unsafe struct ValueUnion
{
[FieldOffset(0)]
public bool boolean;
[FieldOffset(0)]
public byte u8;
[FieldOffset(0)]
public sbyte i8;
[FieldOffset(0)]
public ushort u16;
[FieldOffset(0)]
public short i16;
[FieldOffset(0)]
public uint u32;
[FieldOffset(0)]
public int i32;
[FieldOffset(0)]
public ulong u64;
[FieldOffset(0)]
public long i64;
[FieldOffset(0)]
public float flt;
[FieldOffset(0)]
public double dbl;
[FieldOffset(0)]
public fixed byte data[8];
}
}
public unsafe sealed class ImGuiListClipperEx
{
private ImGuiListClipper *clipper = null;
public int DisplayStart
{
get
{
if(clipper == null)
return 0;
return clipper->DisplayStart;
}
}
public int DisplayEnd
{
get
{
if(clipper == null)
return 0;
return clipper->DisplayEnd;
}
}
public ImGuiListClipperEx()
{
clipper = ImGuiNative.ImGuiListClipper_ImGuiListClipper();
}
public void Begin(int totalCount, float lineHeight)
{
if(clipper != null)
ImGuiNative.ImGuiListClipper_Begin(clipper, totalCount, lineHeight);
}
public bool Step()
{
if(clipper != null)
return ImGuiNative.ImGuiListClipper_Step(clipper) > 0;
return false;
}
public void Dispose()
{
if(clipper != null)
ImGuiNative.ImGuiListClipper_destroy(clipper);
clipper = null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment