Add thread-safety with locks to window manager and rendering code

This commit is contained in:
Stone_Red
2026-06-08 20:18:52 +02:00
parent fd1db1b904
commit 82efb8d967
4 changed files with 222 additions and 156 deletions
+38 -32
View File
@@ -16,6 +16,7 @@ public class TerminalProcess : Process
private readonly List<string> history = new(); private readonly List<string> history = new();
private string currentInput = ""; private string currentInput = "";
private readonly List<Text> textLines = new(); private readonly List<Text> textLines = new();
private readonly object textLinesLock = new object();
private const int LineHeight = 30; private const int LineHeight = 30;
private Size lastSize = new Size(-1, -1); private Size lastSize = new Size(-1, -1);
@@ -26,11 +27,11 @@ public class TerminalProcess : Process
internal override void Run() internal override void Run()
{ {
window = WindowManager.CreateWindow(this, "Terminal", new Point(50, 50), new Size(400, 300)); window = WindowManager.CreateWindow(this, "Terminal", new Point(50, 50), new Size(400, 300));
window.AutoFlush = true;
PrintLine("RemSox GUI Terminal v1.0"); PrintLine("RemSox GUI Terminal v1.0");
PrintLine("Type 'help' for commands."); PrintLine("Type 'help' for commands.");
window.Flush();
window.OnKeyEvent += HandleKey; window.OnKeyEvent += HandleKey;
while (!StopRequested) while (!StopRequested)
@@ -40,7 +41,7 @@ public class TerminalProcess : Process
lastSize = window.Size; lastSize = window.Size;
UpdateDisplay(); UpdateDisplay();
} }
System.Threading.Thread.Sleep(50); Thread.Sleep(50);
} }
window.OnKeyEvent -= HandleKey; window.OnKeyEvent -= HandleKey;
@@ -106,42 +107,47 @@ public class TerminalProcess : Process
if (maxLines < 1) maxLines = 1; if (maxLines < 1) maxLines = 1;
// Ensure we have enough Text elements for maxLines + 1 (input line) lock (textLinesLock)
while (textLines.Count <= maxLines)
{ {
var textElement = window.CreateUIElement<Text>(t => // Ensure we have enough Text elements for maxLines + 1 (input line)
while (textLines.Count <= maxLines)
{ {
t.Color = Color.LightGreen; var textElement = window.CreateUIElement<Text>(t =>
t.Content = ""; {
}); t.Color = Color.LightGreen;
textLines.Add(textElement); t.Content = "";
} });
textLines.Add(textElement);
// Calculate starting Y to align everything flush to the bottom margin
int startY = window.Size.Height - ((maxLines + 1) * LineHeight) - 5;
if (startY < 20) startY = 20;
for (int i = 0; i < maxLines; i++)
{
int historyIndex = history.Count - maxLines + i;
string content = "";
if (historyIndex >= 0 && historyIndex < history.Count)
{
content = history[historyIndex];
} }
textLines[i].Content = content; // Calculate starting Y to align everything flush to the bottom margin
textLines[i].Position = new Point(5, startY + (i * LineHeight)); int startY = window.Size.Height - ((maxLines + 1) * LineHeight) - 5;
} if (startY < 20) startY = 20;
// The last line is the input line for (int i = 0; i < maxLines; i++)
textLines[maxLines].Content = "> " + currentInput + "_"; {
textLines[maxLines].Position = new Point(5, startY + (maxLines * LineHeight)); int historyIndex = history.Count - maxLines + i;
string content = "";
if (historyIndex >= 0 && historyIndex < history.Count)
{
content = history[historyIndex];
}
// Hide any extra text lines we don't need textLines[i].Content = content;
for (int i = maxLines + 1; i < textLines.Count; i++) textLines[i].Position = new Point(5, startY + (i * LineHeight));
{ }
textLines[i].Content = "";
// The last line is the input line
textLines[maxLines].Content = "> " + currentInput + "_";
textLines[maxLines].Position = new Point(5, startY + (maxLines * LineHeight));
// Hide any extra text lines we don't need
for (int i = maxLines + 1; i < textLines.Count; i++)
{
textLines[i].Content = "";
}
window.Flush();
} }
} }
} }
+107 -100
View File
@@ -17,130 +17,137 @@ public sealed class CanvasRenderSource : IRenderSource
private static Point lastPointerPosition = new Point(-1, -1); private static Point lastPointerPosition = new Point(-1, -1);
private static List<int> orderedWindowsCache = new(); private static List<int> orderedWindowsCache = new();
private static bool isZOrderDirty = true; private static bool isZOrderDirty = true;
private static readonly object renderLock = new object();
public void Render(IEnumerable<RenderCommand> commands) public void Render(IEnumerable<RenderCommand> commands)
{ {
bool changed = false; lock (renderLock)
foreach (RenderCommand command in commands)
{ {
changed = true; bool changed = false;
if (command.ElementType == "WindowClose") foreach (RenderCommand command in commands)
{ {
windowCanvases.Remove(command.WindowId); changed = true;
windowPositions.Remove(command.WindowId); if (command.ElementType == "WindowClose")
windowZIndices.Remove(command.WindowId);
isZOrderDirty = true;
continue;
}
if (command.ElementType == "Window" || command.ElementType == "WindowMove")
{
if (command.Properties.TryGetValue("ZIndex", out object? rawZIndex) && rawZIndex is int z)
{ {
windowZIndices[command.WindowId] = z; windowCanvases.Remove(command.WindowId);
windowPositions.Remove(command.WindowId);
windowZIndices.Remove(command.WindowId);
isZOrderDirty = true; isZOrderDirty = true;
continue;
} }
}
if (command.ElementType == "Window") if (command.ElementType == "Window" || command.ElementType == "WindowMove")
{
Size size = command.Properties.TryGetValue("Size", out object? rawSize) && rawSize is Size windowSize
? windowSize
: new Size(160, 120);
if (!windowCanvases.TryGetValue(command.WindowId, out Canvas? currentCanvas) ||
currentCanvas.Mode.Width != size.Width ||
currentCanvas.Mode.Height != size.Height)
{ {
windowCanvases[command.WindowId] = new Canvas(size.Width, size.Height); if (command.Properties.TryGetValue("ZIndex", out object? rawZIndex) && rawZIndex is int z)
{
windowZIndices[command.WindowId] = z;
isZOrderDirty = true;
}
}
if (command.ElementType == "Window")
{
Size size = command.Properties.TryGetValue("Size", out object? rawSize) && rawSize is Size windowSize
? windowSize
: new Size(160, 120);
if (!windowCanvases.TryGetValue(command.WindowId, out Canvas? currentCanvas) ||
currentCanvas.Mode.Width != size.Width ||
currentCanvas.Mode.Height != size.Height)
{
windowCanvases[command.WindowId] = new Canvas(size.Width, size.Height);
}
}
if (!windowCanvases.ContainsKey(command.WindowId))
{
windowCanvases[command.WindowId] = new Canvas(160, 120);
}
Canvas windowCanvas = windowCanvases[command.WindowId];
if (command.ElementType == "Window")
{
RenderWindow(windowCanvas, command);
windowPositions[command.WindowId] = command.Position;
continue;
}
if (command.ElementType == "WindowMove")
{
windowPositions[command.WindowId] = command.Position;
continue;
}
if (command.ElementType == "Circle")
{
RenderCircle(windowCanvas, command);
}
if (command.ElementType == "Rectangle")
{
RenderRectangle(windowCanvas, command);
}
if (command.ElementType == "Text")
{
RenderText(windowCanvas, command);
}
if (command.ElementType == "Line")
{
RenderLine(windowCanvas, command);
}
if (command.ElementType == "Button")
{
RenderButton(windowCanvas, command);
}
if (command.ElementType == "CheckBox")
{
RenderCheckBox(windowCanvas, command);
} }
} }
if (!windowCanvases.ContainsKey(command.WindowId)) if (changed)
{ {
windowCanvases[command.WindowId] = new Canvas(160, 120); isDirty = true;
} }
Canvas windowCanvas = windowCanvases[command.WindowId];
if (command.ElementType == "Window")
{
RenderWindow(windowCanvas, command);
windowPositions[command.WindowId] = command.Position;
continue;
}
if (command.ElementType == "WindowMove")
{
windowPositions[command.WindowId] = command.Position;
continue;
}
if (command.ElementType == "Circle")
{
RenderCircle(windowCanvas, command);
}
if (command.ElementType == "Rectangle")
{
RenderRectangle(windowCanvas, command);
}
if (command.ElementType == "Text")
{
RenderText(windowCanvas, command);
}
if (command.ElementType == "Line")
{
RenderLine(windowCanvas, command);
}
if (command.ElementType == "Button")
{
RenderButton(windowCanvas, command);
}
if (command.ElementType == "CheckBox")
{
RenderCheckBox(windowCanvas, command);
}
}
if (changed)
{
isDirty = true;
} }
} }
public static void CompositeAndDisplay(Canvas screenCanvas, Point pointerPosition) public static void CompositeAndDisplay(Canvas screenCanvas, Point pointerPosition)
{ {
if (!isDirty && pointerPosition == lastPointerPosition) lock (renderLock)
{ {
return; if (!isDirty && pointerPosition == lastPointerPosition)
}
if (isZOrderDirty)
{
orderedWindowsCache = windowPositions.Keys.OrderBy(id => windowZIndices.TryGetValue(id, out int z) ? z : 0).ToList();
isZOrderDirty = false;
}
screenCanvas.Clear(Color.Black);
foreach (var windowId in orderedWindowsCache)
{
if (windowPositions.TryGetValue(windowId, out Point position) && windowCanvases.TryGetValue(windowId, out Canvas? windowCanvas))
{ {
screenCanvas.DrawCanvas(windowCanvas, position.X, position.Y); return;
} }
if (isZOrderDirty)
{
orderedWindowsCache = windowPositions.Keys.OrderBy(id => windowZIndices.TryGetValue(id, out int z) ? z : 0).ToList();
isZOrderDirty = false;
}
screenCanvas.Clear(Color.Black);
foreach (var windowId in orderedWindowsCache)
{
if (windowPositions.TryGetValue(windowId, out Point position) && windowCanvases.TryGetValue(windowId, out Canvas? windowCanvas))
{
screenCanvas.DrawCanvas(windowCanvas, position.X, position.Y);
}
}
screenCanvas.DrawFilledCircle(Color.White, pointerPosition.X, pointerPosition.Y, 5);
screenCanvas.Display();
lastPointerPosition = pointerPosition;
isDirty = false;
} }
screenCanvas.DrawFilledCircle(Color.White, pointerPosition.X, pointerPosition.Y, 5);
screenCanvas.Display();
lastPointerPosition = pointerPosition;
isDirty = false;
} }
private static void RenderWindow(Canvas canvas, RenderCommand command) private static void RenderWindow(Canvas canvas, RenderCommand command)
+15 -3
View File
@@ -46,6 +46,7 @@ public sealed class Window(string title, int processId, int id, IRenderSource re
public bool IsDragging => currentInteraction == InteractionMode.Drag; public bool IsDragging => currentInteraction == InteractionMode.Drag;
private readonly object uiElementsLock = new object();
private readonly Dictionary<int, UIElement> uiElements = []; private readonly Dictionary<int, UIElement> uiElements = [];
private int nextUIElementId = 1; private int nextUIElementId = 1;
@@ -87,7 +88,10 @@ public sealed class Window(string title, int processId, int id, IRenderSource re
Flush(); Flush();
} }
uiElements.Add(uiElementId, uiElement); lock (uiElementsLock)
{
uiElements.Add(uiElementId, uiElement);
}
return uiElement; return uiElement;
} }
@@ -105,8 +109,16 @@ public sealed class Window(string title, int processId, int id, IRenderSource re
return; return;
} }
bool anyChildChanged;
List<UIElement> elementsCopy;
lock (uiElementsLock)
{
anyChildChanged = uiElements.Values.Any(e => e.AnyPropertyChanged);
elementsCopy = uiElements.Values.ToList();
}
bool windowStateChanged = isFirstRender || Size != lastRenderedSize || IsFocused != lastRenderedIsFocused || Title != lastRenderedTitle; bool windowStateChanged = isFirstRender || Size != lastRenderedSize || IsFocused != lastRenderedIsFocused || Title != lastRenderedTitle;
bool anyChildChanged = uiElements.Values.Any(e => e.AnyPropertyChanged);
bool positionChanged = Position != lastRenderedPosition; bool positionChanged = Position != lastRenderedPosition;
bool zIndexChanged = ZIndex != lastRenderedZIndex; bool zIndexChanged = ZIndex != lastRenderedZIndex;
@@ -133,7 +145,7 @@ public sealed class Window(string title, int processId, int id, IRenderSource re
} }
}); });
foreach (UIElement element in uiElements.Values) foreach (UIElement element in elementsCopy)
{ {
commands.Add(new RenderCommand commands.Add(new RenderCommand
{ {
+62 -21
View File
@@ -13,8 +13,9 @@ namespace RemSox.UI.GUI.Windows;
public static class WindowManager public static class WindowManager
{ {
private static readonly object windowsLock = new object();
// Process ID to list of windows // Process ID to list of windows
private static readonly ConcurrentDictionary<int, List<Window>> windows = new(); private static readonly Dictionary<int, List<Window>> windows = new();
private static int nextWindowId = 1; private static int nextWindowId = 1;
private static int nextZIndex = 1; private static int nextZIndex = 1;
@@ -76,21 +77,27 @@ public static class WindowManager
ZIndex = nextZIndex++ ZIndex = nextZIndex++
}; };
if (!windows.ContainsKey(process.Id)) lock (windowsLock)
{ {
windows[process.Id] = []; if (!windows.ContainsKey(process.Id))
} {
windows[process.Id] = [];
}
windows[process.Id].Add(window); windows[process.Id].Add(window);
}
return window; return window;
} }
public static void CloseWindow(Window window) public static void CloseWindow(Window window)
{ {
if (windows.TryGetValue(window.ProcessId, out var processWindows)) lock (windowsLock)
{ {
processWindows.Remove(window); if (windows.TryGetValue(window.ProcessId, out var processWindows))
{
processWindows.Remove(window);
}
} }
renderSource.Render(new[] { new RenderCommand { WindowId = window.Id, ElementId = window.Id, ElementType = "WindowClose", Position = window.Position, Properties = new Dictionary<string, object?>() } }); renderSource.Render(new[] { new RenderCommand { WindowId = window.Id, ElementId = window.Id, ElementType = "WindowClose", Position = window.Position, Properties = new Dictionary<string, object?>() } });
@@ -98,17 +105,23 @@ public static class WindowManager
public static List<Window> GetWindowsForProcess(Process process) public static List<Window> GetWindowsForProcess(Process process)
{ {
if (windows.TryGetValue(process.Id, out var processWindows)) lock (windowsLock)
{ {
return processWindows; if (windows.TryGetValue(process.Id, out var processWindows))
} {
return processWindows.ToList();
}
return []; return [];
}
} }
public static void CloseWindowsForProcess(Process process) public static void CloseWindowsForProcess(Process process)
{ {
windows.TryRemove(process.Id, out _); lock (windowsLock)
{
windows.Remove(process.Id);
}
} }
public static void FocusWindow(Window? window) public static void FocusWindow(Window? window)
@@ -137,7 +150,12 @@ public static class WindowManager
public static Window? TryBeginInteract(Point pointerPosition) public static Window? TryBeginInteract(Point pointerPosition)
{ {
var allWindows = windows.Values.SelectMany(w => w).OrderByDescending(w => w.ZIndex); List<Window> allWindows;
lock (windowsLock)
{
allWindows = windows.Values.SelectMany(w => w).OrderByDescending(w => w.ZIndex).ToList();
}
foreach (var window in allWindows) foreach (var window in allWindows)
{ {
if (window.TryBeginInteract(pointerPosition)) if (window.TryBeginInteract(pointerPosition))
@@ -150,12 +168,15 @@ public static class WindowManager
public static void InvalidateAll() public static void InvalidateAll()
{ {
foreach (var processWindows in windows.Values) List<Window> allWindows;
lock (windowsLock)
{ {
foreach (var window in processWindows) allWindows = windows.Values.SelectMany(w => w).ToList();
{ }
window.Invalidate();
} foreach (var window in allWindows)
{
window.Invalidate();
} }
} }
@@ -166,13 +187,33 @@ public static class WindowManager
sealed private class MuliRenderSource(List<IRenderSource> sources) : IRenderSource sealed private class MuliRenderSource(List<IRenderSource> sources) : IRenderSource
{ {
public void AddSource(IRenderSource source) => sources.Add(source); private readonly object sourcesLock = new object();
public void RemoveSource(IRenderSource source) => sources.Remove(source); public void AddSource(IRenderSource source)
{
lock (sourcesLock)
{
sources.Add(source);
}
}
public void RemoveSource(IRenderSource source)
{
lock (sourcesLock)
{
sources.Remove(source);
}
}
public void Render(IEnumerable<RenderCommand> commands) public void Render(IEnumerable<RenderCommand> commands)
{ {
foreach (IRenderSource source in sources) List<IRenderSource> sourcesCopy;
lock (sourcesLock)
{
sourcesCopy = sources.ToList();
}
foreach (IRenderSource source in sourcesCopy)
{ {
source.Render(commands); source.Render(commands);
} }