using SharpDX; using SharpDX.Direct3D; using SharpDX.Direct3D11; using SharpDX.DXGI; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Windows.Forms; using System.Threading.Tasks; using Buffer = SharpDX.Direct3D11.Buffer; using Color = System.Drawing.Color; using Device = SharpDX.Direct3D11.Device; using Rectangle = System.Drawing.Rectangle; using System.Drawing; using System.Drawing.Imaging; namespace Pulsar.Client.Utilities { /// /// This class provides functions for creating drawing overlays on the screen using Direct3D 11. /// /// Usage: /// 1. Create an instance of ScreenOverlay /// ScreenOverlay overlay = new ScreenOverlay(); /// /// 2. Draw points on the screen: /// // Draw a point at coordinates (100, 100) with 5px width and red color on monitor 0 /// overlay.Draw(100, 100, 200, 200, 5, Color.Red.ToArgb(), 0); /// /// // Each Draw call creates a new point without connecting lines /// // This creates another point at the specified location /// overlay.Draw(200, 200, 300, 150, 5, Color.Red.ToArgb(), 0); /// /// 3. Erase parts of the drawing: /// // Erase area from (150, 150) to (250, 250) with 20px eraser on monitor 0 /// overlay.DrawEraser(150, 150, 250, 250, 20, 0); /// /// 4. Clear all drawings on a monitor: /// overlay.ClearDrawings(0); /// /// 5. Create, move, and rotate 3D cubes: /// // Create a 3D cube with different colors for each face /// overlay.Render3DCube(1, Color.Red.ToArgb(), Color.Blue.ToArgb(), Color.Green.ToArgb(), /// Color.Yellow.ToArgb(), Color.Purple.ToArgb(), Color.Orange.ToArgb(), /// 100, 100, 0, 50, 50); /// /// // Move the cube smoothly to a new position /// overlay.Move3DCube(1, true, 200, 200, 0); /// /// // Rotate the cube (in radians) /// overlay.Rotate3DCube(1, true, 0.5f, 1.0f, 0.25f); /// /// // Remove the cube when done /// overlay.Remove3DCube(1); /// /// 6. Capture screen information: /// // Get pixel color at specific coordinates /// PixelData pixel = overlay.GetPixelCoords(100, 100); /// /// // Get a random pixel from the screen /// PixelData randomPixel = overlay.GetRandomPixel(); /// /// // Capture a region of the screen /// Bitmap screenshot = overlay.CaptureScreenRegion(50, 50, 200, 150); /// /// 7. Custom Rendering: /// // Create and register custom shaders and objects that get rendered on the screen /// // Implement ICustomShader and ICustomRenderable interfaces /// // Then register them with the overlay: /// overlay.RegisterCustomShader(myShader); /// overlay.RegisterCustomRenderable(myRenderable); /// /// // Get DirectX device for more advanced operations /// Device device = overlay.GetDeviceForMonitor(0); /// /// 8. Properly dispose when done: /// overlay.Dispose(); /// /// All drawing operations are performed asynchronously on a background thread for performance. /// The overlay automatically creates transparent windows that appear on top of everything else except the cursor, /// making it suitable for drawing visual elements on any part of the screen. /// /// This implementation uses Direct3D 11. Points are rendered as quads. /// /// Monitor indices correspond to the indices in Screen.AllScreens[], use 0 for primary monitor. /// [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct WNDCLASS { public uint style; public WndProcDelegate lpfnWndProc; public int cbClsExtra; public int cbWndExtra; public IntPtr hInstance; public IntPtr hIcon; public IntPtr hCursor; public IntPtr hbrBackground; [MarshalAs(UnmanagedType.LPStr)] public string lpszMenuName; [MarshalAs(UnmanagedType.LPStr)] public string lpszClassName; } [StructLayout(LayoutKind.Sequential)] public struct POINT { public int x; public int y; } [StructLayout(LayoutKind.Sequential)] public struct SIZE { public int cx; public int cy; } [StructLayout(LayoutKind.Sequential)] public struct PixelData { public Color Color; public int X; public int Y; public int MonitorIndex; public PixelData(Color color, int x, int y, int monitorIndex) { Color = color; X = x; Y = y; MonitorIndex = monitorIndex; } } public delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); /// /// For creating transparent DX overlays on the screen /// public class ScreenOverlay : IDisposable { private Dictionary _overlayWindows = new Dictionary(); private Dictionary _devices = new Dictionary(); private Dictionary _deviceContexts = new Dictionary(); private Dictionary _swapChains = new Dictionary(); private Dictionary _renderTargetViews = new Dictionary(); private Dictionary _shaderResourceViews = new Dictionary(); private Dictionary _renderTextures = new Dictionary(); private Dictionary _needsUpdate = new Dictionary(); private Dictionary _vertexShaders = new Dictionary(); private Dictionary _pixelShaders = new Dictionary(); private Dictionary _inputLayouts = new Dictionary(); private Dictionary _vertexBuffers = new Dictionary(); private Dictionary _vertex3DShaders = new Dictionary(); private Dictionary _pixel3DShaders = new Dictionary(); private Dictionary _input3DLayouts = new Dictionary(); private Dictionary _matrixBuffers = new Dictionary(); private Dictionary> _cubes = new Dictionary>(); private WndProcDelegate _wndProcDelegate; private bool _delegateInit = false; private ConcurrentQueue _drawingQueue = new ConcurrentQueue(); private ManualResetEvent _drawingSignal = new ManualResetEvent(false); private bool _drawingThreadRunning = false; private Thread _drawingThread; private const int MAX_UPDATE_RATE_MS = 16; private const string VertexShaderCode = @" struct VS_INPUT { float2 pos : POSITION; float4 color : COLOR; float size : SIZE; uint vertexID : SV_VertexID; }; struct VS_OUTPUT { float4 pos : SV_POSITION; float4 color : COLOR; }; VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT output; float size = input.size * 0.5f; float2 offset = float2(0, 0); if (input.vertexID % 4 == 0) offset = float2(-size, -size); else if (input.vertexID % 4 == 1) offset = float2(size, -size); else if (input.vertexID % 4 == 2) offset = float2(-size, size); else if (input.vertexID % 4 == 3) offset = float2(size, size); output.pos = float4(input.pos + offset, 0.0f, 1.0f); output.color = input.color; return output; }"; private const string PixelShaderCode = @" struct PS_INPUT { float4 pos : SV_POSITION; float4 color : COLOR; }; float4 main(PS_INPUT input) : SV_TARGET { return input.color; }"; private const string Vertex3DShaderCode = @" cbuffer MatrixBuffer : register(b0) { matrix worldMatrix; matrix viewMatrix; matrix projectionMatrix; }; struct VS_INPUT { float3 position : POSITION; float4 color : COLOR; }; struct VS_OUTPUT { float4 position : SV_POSITION; float4 color : COLOR; }; VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT output; float4 pos = float4(input.position, 1.0f); pos = mul(pos, worldMatrix); pos = mul(pos, viewMatrix); pos = mul(pos, projectionMatrix); output.position = pos; output.color = input.color; return output; }"; private const string Pixel3DShaderCode = @" struct PS_INPUT { float4 position : SV_POSITION; float4 color : COLOR; }; float4 main(PS_INPUT input) : SV_TARGET { return input.color; }"; [StructLayout(LayoutKind.Sequential)] private struct VertexPositionColor { public Vector2 Position; public Color4 Color; public float Size; public VertexPositionColor(Vector2 position, Color4 color, float size) { Position = position; Color = color; Size = size; } public static int SizeInBytes => Marshal.SizeOf(); } [StructLayout(LayoutKind.Sequential)] private struct VertexPosition3DColor { public Vector3 Position; public Color4 Color; public VertexPosition3DColor(Vector3 position, Color4 color) { Position = position; Color = color; } public static int SizeInBytes => Marshal.SizeOf(); } private class MonitorDrawingData { public List Points { get; private set; } = new List(); public int[] Indices { get; private set; } = new int[0]; public int PointCount => Points.Count; private int _bufferCapacity; private readonly int _initialCapacity; public MonitorDrawingData(int initialCapacity = 1000) { _initialCapacity = initialCapacity; _bufferCapacity = initialCapacity; Indices = new int[initialCapacity * 6]; } public void AddPoint(VertexPositionColor point) { Points.Add(point); if (Points.Count > _bufferCapacity) { _bufferCapacity = Points.Count * 2; Debug.WriteLine($"Growing buffer capacity to {_bufferCapacity} points"); } RebuildIndices(); } public void Clear() { Points.Clear(); _bufferCapacity = _initialCapacity; } public void RebuildIndices() { int pointCount = Points.Count; if (Indices.Length < pointCount * 6) { Indices = new int[pointCount * 6]; } for (int i = 0; i < pointCount; i++) { int baseIndex = i * 4; int indexOffset = i * 6; Indices[indexOffset] = baseIndex; Indices[indexOffset + 1] = baseIndex + 1; Indices[indexOffset + 2] = baseIndex + 2; Indices[indexOffset + 3] = baseIndex + 1; Indices[indexOffset + 4] = baseIndex + 3; Indices[indexOffset + 5] = baseIndex + 2; } } public int GetUsedIndexCount() => Math.Min(Points.Count * 6, Indices.Length); } private Dictionary _drawingData = new Dictionary(); private Dictionary _indexBuffers = new Dictionary(); private Dictionary _bufferSizes = new Dictionary(); private const int INITIAL_BUFFER_SIZE = 1000; private Vector3 _cameraPosition = new Vector3(0, 0, -5); private Vector3 _cameraTarget = new Vector3(0, 0, 0); private Vector3 _cameraUp = new Vector3(0, 1, 0); private float _fieldOfView = (float)Math.PI / 4.0f; private float _aspectRatio = 1.0f; private float _nearPlane = 0.1f; private float _farPlane = 1000.0f; private Dictionary> _customShaders = new Dictionary>(); private Dictionary> _customRenderables = new Dictionary>(); /// /// Creates a new ScreenOverlay instance /// public ScreenOverlay() { drawThreadRunning(); } /// /// Checks if drawing thread is running /// private void drawThreadRunning() { if (!_drawingThreadRunning) { lock (_drawingQueue) { if (!_drawingThreadRunning) { _drawingThreadRunning = true; _drawingThread = new Thread(DrawingThreadProc); _drawingThread.IsBackground = true; _drawingThread.Start(); } } } } /// /// Thread proc for handling drawing operations asynchronously /// private void DrawingThreadProc() { try { Stopwatch updateTimer = new Stopwatch(); updateTimer.Start(); while (_drawingThreadRunning) { try { _drawingSignal.WaitOne(5); bool didWork = false; int maxOperationsPerBatch = 20; int processedOps = 0; while (processedOps < maxOperationsPerBatch && _drawingQueue.TryDequeue(out Action drawAction)) { try { drawAction(); didWork = true; processedOps++; } catch (Exception) { } } long elapsedMs = updateTimer.ElapsedMilliseconds; if (elapsedMs >= MAX_UPDATE_RATE_MS) { updateTimer.Restart(); UpdateActiveOverlays(); } if (!didWork) { Thread.Sleep(1); } if (_drawingQueue.IsEmpty) { _drawingSignal.Reset(); } } catch (Exception) { Thread.Sleep(100); } } } catch (Exception) { _drawingThreadRunning = false; } } /// /// Queues a drawing operation to be async executed /// public void QueueDrawingAction(Action drawAction) { if (drawAction == null) return; drawThreadRunning(); _drawingQueue.Enqueue(drawAction); _drawingSignal.Set(); } /// /// Draws a point on a monitor. The strokeWidth parameter controls the size of the point. /// /// Previous X coordinate (ignored in Direct3D implementation which only x,y are used) /// Previous Y coordinate (ignored in Direct3D implementation which only x,y are used) /// X coordinate to draw at /// Y coordinate to draw at /// Width/size of the point /// ARGB color value /// Monitor index to draw on public void Draw(int prevX, int prevY, int x, int y, int strokeWidth, int colorArgb, int monitorIndex) { QueueDrawingAction(() => DrawPoint(monitorIndex, x, y, colorArgb, strokeWidth)); } /// /// Erases at the specified coordinates on a monitor by removing any points in the radius determined by strokeWidth /// /// Previous X coordinate (ignored in Direct3D implementation which only x,y are used) /// Previous Y coordinate (ignored in Direct3D implementation which only x,y are used) /// X coordinate to erase at /// Y coordinate to erase at /// Width of the eraser (radius) /// Monitor index to erase on public void DrawEraser(int prevX, int prevY, int x, int y, int strokeWidth, int monitorIndex) { QueueDrawingAction(() => Erase(monitorIndex, x, y, strokeWidth)); } /// /// Draws a point on the screen at the specified coordinates /// private void DrawPoint(int monitorIndex, int x, int y, int colorArgb, float strokeWidth) { try { Rectangle bounds; try { bounds = Screen.AllScreens[monitorIndex].Bounds; } catch (IndexOutOfRangeException) { return; } ShowDrawingOverlay(monitorIndex, bounds); if (x < 0 || y < 0 || x >= bounds.Width || y >= bounds.Height) { return; } float ndcX = (x / (float)bounds.Width) * 2.0f - 1.0f; float ndcY = 1.0f - (y / (float)bounds.Height) * 2.0f; Color argbColor = Color.FromArgb(colorArgb); var color4 = new Color4(argbColor.R / 255.0f, argbColor.G / 255.0f, argbColor.B / 255.0f, argbColor.A / 255.0f); float scaledWidth = (strokeWidth / (float)bounds.Width) * 2.0f; float pointSize = Math.Max(0.01f, scaledWidth * 5.0f); var vertex = new VertexPositionColor( new Vector2(ndcX, ndcY), color4, pointSize ); if (!_drawingData.TryGetValue(monitorIndex, out MonitorDrawingData data)) { data = new MonitorDrawingData(INITIAL_BUFFER_SIZE); _drawingData[monitorIndex] = data; } data.AddPoint(vertex); _needsUpdate[monitorIndex] = true; } catch (Exception ex) { Debug.WriteLine($"DrawPoint error: {ex.Message}"); } } /// /// Erases at the specified coordinates /// private void Erase(int monitorIndex, int x, int y, float strokeWidth) { try { Rectangle bounds; try { bounds = Screen.AllScreens[monitorIndex].Bounds; } catch (IndexOutOfRangeException) { return; } bool shouldUpdate = false; bool isRemovingLastPoint = false; if (_drawingData.TryGetValue(monitorIndex, out MonitorDrawingData data)) { shouldUpdate = true; if (data.Points.Count > 0) { float ndcX = (x / (float)bounds.Width) * 2.0f - 1.0f; float ndcY = 1.0f - (y / (float)bounds.Height) * 2.0f; float eraseRadiusX = (strokeWidth / (float)bounds.Width) * 2.0f * 1.5f; float eraseRadiusY = (strokeWidth / (float)bounds.Height) * 2.0f * 1.5f; List remainingPoints = new List(data.Points.Count); float radiusSquared = eraseRadiusX * eraseRadiusX; for (int i = 0; i < data.Points.Count; i++) { var point = data.Points[i]; float dx = point.Position.X - ndcX; float dy = point.Position.Y - ndcY; float distanceSquared = dx * dx + dy * dy; float pointRadius = point.Size / (float)Math.Max(bounds.Width, bounds.Height); if (distanceSquared > radiusSquared + pointRadius * pointRadius) { remainingPoints.Add(point); } } if (remainingPoints.Count == 0 && data.Points.Count > 0) { isRemovingLastPoint = true; } if (remainingPoints.Count < data.Points.Count) { data.Points.Clear(); data.Points.AddRange(remainingPoints); data.RebuildIndices(); } } } if (shouldUpdate) { _needsUpdate[monitorIndex] = true; if (isRemovingLastPoint) { ForceImmediateUpdate(monitorIndex); } } } catch (Exception ex) { Debug.WriteLine($"Erase error: {ex.Message}"); } } private void ForceImmediateUpdate(int monitorIndex) { try { if (_deviceContexts.TryGetValue(monitorIndex, out DeviceContext context) && _renderTargetViews.TryGetValue(monitorIndex, out RenderTargetView renderTargetView) && _swapChains.TryGetValue(monitorIndex, out SwapChain swapChain)) { context.ClearRenderTargetView(renderTargetView, new Color4(0, 0, 0, 0)); context.OutputMerger.SetRenderTargets(renderTargetView); swapChain.Present(0, PresentFlags.None); } } catch (Exception ex) { Debug.WriteLine($"ForceImmediateUpdate error: {ex.Message}"); } } /// /// Clears all drawings on a monitor /// public void ClearDrawings(int monitorIndex) { QueueDrawingAction(() => { try { if (_drawingData.TryGetValue(monitorIndex, out MonitorDrawingData data)) { data.Clear(); _needsUpdate[monitorIndex] = true; } if (_deviceContexts.TryGetValue(monitorIndex, out DeviceContext context) && _renderTargetViews.TryGetValue(monitorIndex, out RenderTargetView renderTargetView)) { context.ClearRenderTargetView(renderTargetView, new Color4(0, 0, 0, 0)); if (_swapChains.TryGetValue(monitorIndex, out SwapChain swapChain)) { swapChain.Present(0, PresentFlags.None); } } RenderOverlay(monitorIndex); } catch (Exception ex) { Debug.WriteLine($"ClearDrawings error: {ex.Message}"); } }); } /// /// Creates the overlay window and DirectX resources /// private void ShowDrawingOverlay(int monitorIndex, Rectangle bounds) { if (_overlayWindows.TryGetValue(monitorIndex, out IntPtr hwnd) && hwnd != IntPtr.Zero) { return; } if (!_delegateInit) { _wndProcDelegate = DXOverlayWndProc; _delegateInit = true; } string className = $"{monitorIndex}"; WNDCLASS wndClass = new WNDCLASS(); wndClass.lpfnWndProc = _wndProcDelegate; wndClass.hInstance = NativeMethods.GetModuleHandle(null); wndClass.lpszClassName = className; wndClass.hbrBackground = IntPtr.Zero; try { NativeMethods.RegisterClass(ref wndClass); } catch (Exception ex) { Debug.WriteLine($"RegisterClass error: {ex.Message}"); } hwnd = NativeMethods.CreateWindowEx( NativeMethods.WS_EX_LAYERED | NativeMethods.WS_EX_TOOLWINDOW | NativeMethods.WS_EX_TRANSPARENT, className, "", NativeMethods.WS_POPUP, bounds.X, bounds.Y, bounds.Width, bounds.Height, IntPtr.Zero, IntPtr.Zero, NativeMethods.GetModuleHandle(null), IntPtr.Zero); if (hwnd == IntPtr.Zero) { Debug.WriteLine("Failed to create overlay window"); return; } _overlayWindows[monitorIndex] = hwnd; NativeMethods.SetLayeredWindowAttributes(hwnd, 0, 255, NativeMethods.LWA_COLORKEY | NativeMethods.LWA_ALPHA); NativeMethods.ShowWindow(hwnd, NativeMethods.SW_SHOWNA); NativeMethods.SetWindowPos( hwnd, NativeMethods.HWND_TOPMOST, 0, 0, 0, 0, NativeMethods.SWP_NOMOVE | NativeMethods.SWP_NOSIZE | NativeMethods.SWP_NOACTIVATE ); CreateDXResources(monitorIndex, hwnd, bounds); } /// /// Create DirectX resources for a specific monitor overlay including swap chain, buffers, and shaders /// private void CreateDXResources(int monitorIndex, IntPtr windowHandle, Rectangle bounds) { try { var swapChainDesc = new SwapChainDescription { BufferCount = 2, Usage = Usage.RenderTargetOutput, OutputHandle = windowHandle, IsWindowed = true, ModeDescription = new ModeDescription( bounds.Width, bounds.Height, new Rational(60, 1), Format.B8G8R8A8_UNorm ), SampleDescription = new SampleDescription(1, 0), Flags = SwapChainFlags.AllowModeSwitch, SwapEffect = SwapEffect.Discard }; Device device; SwapChain swapChain; try { Device.CreateWithSwapChain( DriverType.Hardware, DeviceCreationFlags.BgraSupport, swapChainDesc, out device, out swapChain ); } catch (Exception ex) { Debug.WriteLine($"failed to create device & swap chain: {ex.Message}"); return; } _devices[monitorIndex] = device; _swapChains[monitorIndex] = swapChain; var context = device.ImmediateContext; _deviceContexts[monitorIndex] = context; try { using (var backBuffer = swapChain.GetBackBuffer(0)) { var renderTargetView = new RenderTargetView(device, backBuffer); _renderTargetViews[monitorIndex] = renderTargetView; } } catch (Exception ex) { Debug.WriteLine($"failed to create render target view: {ex.Message}"); return; } context.Rasterizer.SetViewport(0, 0, bounds.Width, bounds.Height); try { var blendStateDesc = new BlendStateDescription(); blendStateDesc.AlphaToCoverageEnable = false; blendStateDesc.IndependentBlendEnable = false; for (int i = 0; i < 8; i++) { blendStateDesc.RenderTarget[i].IsBlendEnabled = true; blendStateDesc.RenderTarget[i].SourceBlend = BlendOption.SourceAlpha; blendStateDesc.RenderTarget[i].DestinationBlend = BlendOption.InverseSourceAlpha; blendStateDesc.RenderTarget[i].BlendOperation = BlendOperation.Add; blendStateDesc.RenderTarget[i].SourceAlphaBlend = BlendOption.One; blendStateDesc.RenderTarget[i].DestinationAlphaBlend = BlendOption.Zero; blendStateDesc.RenderTarget[i].AlphaBlendOperation = BlendOperation.Add; blendStateDesc.RenderTarget[i].RenderTargetWriteMask = ColorWriteMaskFlags.All; } var blendState = new BlendState(device, blendStateDesc); context.OutputMerger.SetBlendState(blendState); } catch (Exception ex) { Debug.WriteLine($"failed to set blend state: {ex.Message}"); } if (!CompileShaders(device, monitorIndex)) { return; } try { var vertexBufferDesc = new BufferDescription { SizeInBytes = VertexPositionColor.SizeInBytes * INITIAL_BUFFER_SIZE * 4, Usage = ResourceUsage.Dynamic, BindFlags = BindFlags.VertexBuffer, CpuAccessFlags = CpuAccessFlags.Write, OptionFlags = ResourceOptionFlags.None }; var vertexBuffer = new Buffer(device, vertexBufferDesc); _vertexBuffers[monitorIndex] = vertexBuffer; var indexBufferDesc = new BufferDescription { SizeInBytes = sizeof(int) * INITIAL_BUFFER_SIZE * 6, Usage = ResourceUsage.Dynamic, BindFlags = BindFlags.IndexBuffer, CpuAccessFlags = CpuAccessFlags.Write, OptionFlags = ResourceOptionFlags.None }; var indexBuffer = new Buffer(device, indexBufferDesc); _indexBuffers[monitorIndex] = indexBuffer; _bufferSizes[monitorIndex] = INITIAL_BUFFER_SIZE; } catch (Exception ex) { Debug.WriteLine($"Failed to create buffers: {ex.Message}"); return; } _drawingData[monitorIndex] = new MonitorDrawingData(INITIAL_BUFFER_SIZE); _needsUpdate[monitorIndex] = true; try { var rasterizerDesc = new RasterizerStateDescription { CullMode = CullMode.None, FillMode = FillMode.Solid, IsAntialiasedLineEnabled = true, IsMultisampleEnabled = true, IsFrontCounterClockwise = false, IsScissorEnabled = false, IsDepthClipEnabled = false }; var rasterState = new RasterizerState(device, rasterizerDesc); context.Rasterizer.State = rasterState; } catch (Exception ex) { Debug.WriteLine($"failed to set rasterizer state: {ex.Message}"); } Setup3DResources(monitorIndex); } catch (Exception ex) { Debug.WriteLine($"CreateDXResources error: {ex.Message}"); } } /// /// Sets up the 3D resources for the overlay /// private void Setup3DResources(int monitorIndex) { if (!_devices.TryGetValue(monitorIndex, out Device device)) { Debug.WriteLine($"No device available for monitor {monitorIndex}"); return; } var depthDesc = new Texture2DDescription { Width = Screen.AllScreens[monitorIndex].Bounds.Width, Height = Screen.AllScreens[monitorIndex].Bounds.Height, MipLevels = 1, ArraySize = 1, Format = Format.D24_UNorm_S8_UInt, SampleDescription = new SampleDescription(1, 0), Usage = ResourceUsage.Default, BindFlags = BindFlags.DepthStencil, CpuAccessFlags = CpuAccessFlags.None, OptionFlags = ResourceOptionFlags.None }; var depthTexture = new Texture2D(device, depthDesc); var depthStencilView = new DepthStencilView(device, depthTexture); _deviceContexts[monitorIndex].OutputMerger.SetDepthStencilState( new DepthStencilState(device, new DepthStencilStateDescription { IsDepthEnabled = true, DepthWriteMask = DepthWriteMask.All, DepthComparison = Comparison.Less, IsStencilEnabled = false }) ); _deviceContexts[monitorIndex].OutputMerger.SetRenderTargets(depthStencilView, _renderTargetViews[monitorIndex]); _aspectRatio = (float)Screen.AllScreens[monitorIndex].Bounds.Width / Screen.AllScreens[monitorIndex].Bounds.Height; if (!_vertex3DShaders.ContainsKey(monitorIndex)) { try { using (var vertexShaderBytecode = SharpDX.D3DCompiler.ShaderBytecode.Compile( Vertex3DShaderCode, "main", "vs_4_0", SharpDX.D3DCompiler.ShaderFlags.Debug)) { _vertex3DShaders[monitorIndex] = new VertexShader(device, vertexShaderBytecode); var inputElements = new[] { new InputElement("POSITION", 0, Format.R32G32B32_Float, 0, 0), new InputElement("COLOR", 0, Format.R32G32B32A32_Float, 12, 0) }; _input3DLayouts[monitorIndex] = new InputLayout(device, vertexShaderBytecode, inputElements); } using (var pixelShaderBytecode = SharpDX.D3DCompiler.ShaderBytecode.Compile( Pixel3DShaderCode, "main", "ps_4_0", SharpDX.D3DCompiler.ShaderFlags.Debug)) { _pixel3DShaders[monitorIndex] = new PixelShader(device, pixelShaderBytecode); } var matrixBufferDesc = new BufferDescription { Usage = ResourceUsage.Dynamic, SizeInBytes = SharpDX.Utilities.SizeOf() * 3, BindFlags = BindFlags.ConstantBuffer, CpuAccessFlags = CpuAccessFlags.Write, OptionFlags = ResourceOptionFlags.None, StructureByteStride = 0 }; _matrixBuffers[monitorIndex] = new Buffer(device, matrixBufferDesc); _cubes[monitorIndex] = new Dictionary(); } catch (Exception ex) { Debug.WriteLine($"Error setting up 3D resources: {ex.Message}"); } } } /// /// Creates a cube mesh with the specified colors for each face /// private void CreateCubeMesh(Device device, Cube3DData cube) { VertexPosition3DColor[] vertices = new VertexPosition3DColor[24]; float halfSize = 0.5f; vertices[0] = new VertexPosition3DColor(new Vector3(-halfSize, -halfSize, halfSize), cube.FrontColor); vertices[1] = new VertexPosition3DColor(new Vector3(-halfSize, halfSize, halfSize), cube.FrontColor); vertices[2] = new VertexPosition3DColor(new Vector3(halfSize, halfSize, halfSize), cube.FrontColor); vertices[3] = new VertexPosition3DColor(new Vector3(halfSize, -halfSize, halfSize), cube.FrontColor); vertices[4] = new VertexPosition3DColor(new Vector3(halfSize, -halfSize, -halfSize), cube.BackColor); vertices[5] = new VertexPosition3DColor(new Vector3(halfSize, halfSize, -halfSize), cube.BackColor); vertices[6] = new VertexPosition3DColor(new Vector3(-halfSize, halfSize, -halfSize), cube.BackColor); vertices[7] = new VertexPosition3DColor(new Vector3(-halfSize, -halfSize, -halfSize), cube.BackColor); vertices[8] = new VertexPosition3DColor(new Vector3(-halfSize, halfSize, halfSize), cube.TopColor); vertices[9] = new VertexPosition3DColor(new Vector3(-halfSize, halfSize, -halfSize), cube.TopColor); vertices[10] = new VertexPosition3DColor(new Vector3(halfSize, halfSize, -halfSize), cube.TopColor); vertices[11] = new VertexPosition3DColor(new Vector3(halfSize, halfSize, halfSize), cube.TopColor); vertices[12] = new VertexPosition3DColor(new Vector3(-halfSize, -halfSize, -halfSize), cube.BottomColor); vertices[13] = new VertexPosition3DColor(new Vector3(-halfSize, -halfSize, halfSize), cube.BottomColor); vertices[14] = new VertexPosition3DColor(new Vector3(halfSize, -halfSize, halfSize), cube.BottomColor); vertices[15] = new VertexPosition3DColor(new Vector3(halfSize, -halfSize, -halfSize), cube.BottomColor); vertices[16] = new VertexPosition3DColor(new Vector3(-halfSize, -halfSize, -halfSize), cube.LeftColor); vertices[17] = new VertexPosition3DColor(new Vector3(-halfSize, halfSize, -halfSize), cube.LeftColor); vertices[18] = new VertexPosition3DColor(new Vector3(-halfSize, halfSize, halfSize), cube.LeftColor); vertices[19] = new VertexPosition3DColor(new Vector3(-halfSize, -halfSize, halfSize), cube.LeftColor); vertices[20] = new VertexPosition3DColor(new Vector3(halfSize, -halfSize, halfSize), cube.RightColor); vertices[21] = new VertexPosition3DColor(new Vector3(halfSize, halfSize, halfSize), cube.RightColor); vertices[22] = new VertexPosition3DColor(new Vector3(halfSize, halfSize, -halfSize), cube.RightColor); vertices[23] = new VertexPosition3DColor(new Vector3(halfSize, -halfSize, -halfSize), cube.RightColor); var vertexBufferDesc = new BufferDescription { SizeInBytes = VertexPosition3DColor.SizeInBytes * vertices.Length, Usage = ResourceUsage.Default, BindFlags = BindFlags.VertexBuffer, CpuAccessFlags = CpuAccessFlags.None, OptionFlags = ResourceOptionFlags.None }; cube.VertexBuffer = Buffer.Create(device, vertices, vertexBufferDesc); cube.VertexCount = vertices.Length; int[] indices = new int[36]; indices[0] = 0; indices[1] = 1; indices[2] = 2; indices[3] = 0; indices[4] = 2; indices[5] = 3; indices[6] = 4; indices[7] = 5; indices[8] = 6; indices[9] = 4; indices[10] = 6; indices[11] = 7; indices[12] = 8; indices[13] = 9; indices[14] = 10; indices[15] = 8; indices[16] = 10; indices[17] = 11; indices[18] = 12; indices[19] = 13; indices[20] = 14; indices[21] = 12; indices[22] = 14; indices[23] = 15; indices[24] = 16; indices[25] = 17; indices[26] = 18; indices[27] = 16; indices[28] = 18; indices[29] = 19; indices[30] = 20; indices[31] = 21; indices[32] = 22; indices[33] = 20; indices[34] = 22; indices[35] = 23; var indexBufferDesc = new BufferDescription { SizeInBytes = sizeof(int) * indices.Length, Usage = ResourceUsage.Default, BindFlags = BindFlags.IndexBuffer, CpuAccessFlags = CpuAccessFlags.None, OptionFlags = ResourceOptionFlags.None }; cube.IndexBuffer = Buffer.Create(device, indices, indexBufferDesc); cube.IndexCount = indices.Length; } /// /// Compile vertex and pixel shaders at runtime /// private bool CompileShaders(Device device, int monitorIndex) { try { using (var vertexShaderBytecode = SharpDX.D3DCompiler.ShaderBytecode.Compile( VertexShaderCode, "main", "vs_4_0", SharpDX.D3DCompiler.ShaderFlags.Debug)) { var vertexShader = new VertexShader(device, vertexShaderBytecode); _vertexShaders[monitorIndex] = vertexShader; var inputElements = new[] { new InputElement("POSITION", 0, Format.R32G32_Float, 0, 0), new InputElement("COLOR", 0, Format.R32G32B32A32_Float, 8, 0), new InputElement("SIZE", 0, Format.R32_Float, 24, 0) }; var inputLayout = new InputLayout(device, vertexShaderBytecode, inputElements); _inputLayouts[monitorIndex] = inputLayout; } using (var pixelShaderBytecode = SharpDX.D3DCompiler.ShaderBytecode.Compile( PixelShaderCode, "main", "ps_4_0", SharpDX.D3DCompiler.ShaderFlags.Debug)) { var pixelShader = new PixelShader(device, pixelShaderBytecode); _pixelShaders[monitorIndex] = pixelShader; } return true; } catch (Exception ex) { Debug.WriteLine($"CompileShaders error: {ex.Message}"); return false; } } /// /// Update all active overlays /// private void UpdateActiveOverlays() { UpdateAnimatedCubes(); HashSet monitorsToUpdate = new HashSet(); foreach (var kvp in _needsUpdate) { if (kvp.Value) { monitorsToUpdate.Add(kvp.Key); } } foreach (var monitorIndex in monitorsToUpdate) { try { RenderOverlay(monitorIndex); _needsUpdate[monitorIndex] = false; } catch (Exception ex) { Debug.WriteLine($"UpdateActiveOverlays error: {ex.Message}"); } } } /// /// Renders the overlay for a monitor /// private void RenderOverlay(int monitorIndex) { try { if (!_deviceContexts.TryGetValue(monitorIndex, out DeviceContext context) || !_renderTargetViews.TryGetValue(monitorIndex, out RenderTargetView renderTargetView) || !_vertexShaders.TryGetValue(monitorIndex, out VertexShader vertexShader) || !_pixelShaders.TryGetValue(monitorIndex, out PixelShader pixelShader) || !_inputLayouts.TryGetValue(monitorIndex, out InputLayout inputLayout) || !_vertexBuffers.TryGetValue(monitorIndex, out Buffer vertexBuffer) || !_indexBuffers.TryGetValue(monitorIndex, out Buffer indexBuffer) || !_swapChains.TryGetValue(monitorIndex, out SwapChain swapChain) || !_drawingData.TryGetValue(monitorIndex, out MonitorDrawingData drawingData)) { Debug.WriteLine($"Missing DirectX resources for monitor {monitorIndex}"); return; } if (drawingData.Points.Count == 0) { context.ClearRenderTargetView(renderTargetView, new Color4(0, 0, 0, 0)); bool hasContent = false; if (_cubes.TryGetValue(monitorIndex, out Dictionary cubes) && cubes.Count > 0) { Render3DCubes(monitorIndex, context, cubes.Values.ToList()); hasContent = true; } if (_customRenderables.TryGetValue(monitorIndex, out Dictionary renderables) && renderables.Count > 0) { RenderCustomRenderables(monitorIndex, context, renderables.Values.ToList()); hasContent = true; } if (hasContent) { swapChain.Present(1, PresentFlags.DoNotWait); } return; } int pointCount = drawingData.Points.Count; int vertexCount = pointCount * 4; int usedIndices = pointCount * 6; if (!_bufferSizes.TryGetValue(monitorIndex, out int currentBufferSize) || vertexCount > currentBufferSize * 4) { int newBufferSize = Math.Max(pointCount * 2, INITIAL_BUFFER_SIZE); try { Debug.WriteLine($"resizing buffers for monitor {monitorIndex}: " + $"points={pointCount}, new capacity={newBufferSize}"); var newVertexBufferDesc = new BufferDescription { SizeInBytes = VertexPositionColor.SizeInBytes * newBufferSize * 4, Usage = ResourceUsage.Dynamic, BindFlags = BindFlags.VertexBuffer, CpuAccessFlags = CpuAccessFlags.Write, OptionFlags = ResourceOptionFlags.None }; var newVertexBuffer = new Buffer(context.Device, newVertexBufferDesc); var newIndexBufferDesc = new BufferDescription { SizeInBytes = sizeof(int) * newBufferSize * 6, Usage = ResourceUsage.Dynamic, BindFlags = BindFlags.IndexBuffer, CpuAccessFlags = CpuAccessFlags.Write, OptionFlags = ResourceOptionFlags.None }; var newIndexBuffer = new Buffer(context.Device, newIndexBufferDesc); vertexBuffer.Dispose(); indexBuffer.Dispose(); _vertexBuffers[monitorIndex] = newVertexBuffer; _indexBuffers[monitorIndex] = newIndexBuffer; _bufferSizes[monitorIndex] = newBufferSize; vertexBuffer = newVertexBuffer; indexBuffer = newIndexBuffer; } catch (Exception ex) { Debug.WriteLine($"Failed to resize buffers: {ex.Message}"); } } try { var quadVertices = new VertexPositionColor[vertexCount]; for (int i = 0; i < pointCount; i++) { var point = drawingData.Points[i]; float halfSize = point.Size * 0.5f; quadVertices[i * 4] = new VertexPositionColor( new Vector2(point.Position.X - halfSize, point.Position.Y - halfSize), point.Color, point.Size ); quadVertices[i * 4 + 1] = new VertexPositionColor( new Vector2(point.Position.X + halfSize, point.Position.Y - halfSize), point.Color, point.Size ); quadVertices[i * 4 + 2] = new VertexPositionColor( new Vector2(point.Position.X - halfSize, point.Position.Y + halfSize), point.Color, point.Size ); quadVertices[i * 4 + 3] = new VertexPositionColor( new Vector2(point.Position.X + halfSize, point.Position.Y + halfSize), point.Color, point.Size ); } try { var vertexDataBox = context.MapSubresource( vertexBuffer, 0, MapMode.WriteDiscard, SharpDX.Direct3D11.MapFlags.None); SharpDX.Utilities.Write(vertexDataBox.DataPointer, quadVertices, 0, vertexCount); context.UnmapSubresource(vertexBuffer, 0); } catch (Exception ex) { Debug.WriteLine($"Error updating vertex buffer: {ex.Message}"); return; } try { var indexDataBox = context.MapSubresource( indexBuffer, 0, MapMode.WriteDiscard, SharpDX.Direct3D11.MapFlags.None); SharpDX.Utilities.Write(indexDataBox.DataPointer, drawingData.Indices, 0, usedIndices); context.UnmapSubresource(indexBuffer, 0); } catch (Exception ex) { Debug.WriteLine($"Error updating index buffer: {ex.Message}"); return; } context.ClearRenderTargetView(renderTargetView, new Color4(0, 0, 0, 0)); context.OutputMerger.SetRenderTargets(renderTargetView); context.VertexShader.Set(vertexShader); context.PixelShader.Set(pixelShader); context.InputAssembler.InputLayout = inputLayout; context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList; context.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(vertexBuffer, VertexPositionColor.SizeInBytes, 0)); context.InputAssembler.SetIndexBuffer(indexBuffer, Format.R32_UInt, 0); context.DrawIndexed(usedIndices, 0, 0); if (_cubes.TryGetValue(monitorIndex, out Dictionary cubes) && cubes.Count > 0) { Render3DCubes(monitorIndex, context, cubes.Values.ToList()); } if (_customRenderables.TryGetValue(monitorIndex, out Dictionary renderables) && renderables.Count > 0) { RenderCustomRenderables(monitorIndex, context, renderables.Values.ToList()); } swapChain.Present(1, PresentFlags.DoNotWait); } catch (SharpDXException dx) { Debug.WriteLine($"DirectX error in RenderOverlay: {dx.Message}"); } catch (Exception ex) { Debug.WriteLine($"RenderOverlay error: {ex.Message}"); } } catch (Exception ex) { Debug.WriteLine($"RenderOverlay outer error: {ex.Message}"); } } /// /// Renders the 3D cubes for a monitor /// private void Render3DCubes(int monitorIndex, DeviceContext context, List cubes) { try { if (!_vertex3DShaders.TryGetValue(monitorIndex, out VertexShader vertexShader) || !_pixel3DShaders.TryGetValue(monitorIndex, out PixelShader pixelShader) || !_input3DLayouts.TryGetValue(monitorIndex, out InputLayout inputLayout) || !_matrixBuffers.TryGetValue(monitorIndex, out Buffer matrixBuffer)) { Setup3DResources(monitorIndex); if (!_vertex3DShaders.TryGetValue(monitorIndex, out vertexShader) || !_pixel3DShaders.TryGetValue(monitorIndex, out pixelShader) || !_input3DLayouts.TryGetValue(monitorIndex, out inputLayout) || !_matrixBuffers.TryGetValue(monitorIndex, out matrixBuffer)) { return; } } context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList; context.VertexShader.Set(vertexShader); context.PixelShader.Set(pixelShader); context.InputAssembler.InputLayout = inputLayout; Matrix viewMatrix = Matrix.LookAtLH(_cameraPosition, _cameraTarget, _cameraUp); Matrix projectionMatrix = Matrix.PerspectiveFovLH( _fieldOfView, _aspectRatio, _nearPlane, _farPlane ); foreach (var cube in cubes) { DataStream mappedResource; context.MapSubresource(matrixBuffer, MapMode.WriteDiscard, SharpDX.Direct3D11.MapFlags.None, out mappedResource); mappedResource.Write(cube.WorldMatrix); mappedResource.Write(viewMatrix); mappedResource.Write(projectionMatrix); context.UnmapSubresource(matrixBuffer, 0); context.VertexShader.SetConstantBuffer(0, matrixBuffer); context.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(cube.VertexBuffer, VertexPosition3DColor.SizeInBytes, 0)); context.InputAssembler.SetIndexBuffer(cube.IndexBuffer, Format.R32_UInt, 0); context.DrawIndexed(cube.IndexCount, 0, 0); } } catch (Exception ex) { Debug.WriteLine($"Render3DCubes error: {ex.Message}"); } } /// /// Renders custom renderables for a monitor /// private void RenderCustomRenderables(int monitorIndex, DeviceContext context, List renderables) { try { Matrix viewMatrix = Matrix.LookAtLH(_cameraPosition, _cameraTarget, _cameraUp); Matrix projectionMatrix = Matrix.PerspectiveFovLH( _fieldOfView, _aspectRatio, _nearPlane, _farPlane ); bool customShaderApplied = false; if (_customShaders.TryGetValue(monitorIndex, out Dictionary shaders) && shaders.Count > 0) { foreach (var shader in shaders.Values) { try { shader.Apply(context); customShaderApplied = true; break; } catch (Exception ex) { Debug.WriteLine($"Error applying custom shader {shader.ShaderId}: {ex.Message}"); } } } foreach (var renderable in renderables) { try { renderable.Render(context, viewMatrix, projectionMatrix); renderable.NeedsUpdate = false; } catch (Exception ex) { Debug.WriteLine($"Error rendering custom renderable {renderable.RenderableId}: {ex.Message}"); } } if (customShaderApplied && _vertexShaders.TryGetValue(monitorIndex, out VertexShader defaultVS) && _pixelShaders.TryGetValue(monitorIndex, out PixelShader defaultPS)) { context.VertexShader.Set(defaultVS); context.PixelShader.Set(defaultPS); } } catch (Exception ex) { Debug.WriteLine($"RenderCustomRenderables error: {ex.Message}"); } } private IntPtr DXOverlayWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { switch (msg) { case NativeMethods.WM_DESTROY: try { var toRemove = new List(); foreach (var pair in _overlayWindows) { if (pair.Value == hWnd) { toRemove.Add(pair.Key); CleanupDirectXResources(pair.Key); } } foreach (var key in toRemove) { _overlayWindows.Remove(key); } } catch (Exception ex) { Debug.WriteLine($"WndProc error: {ex.Message}"); } break; } return NativeMethods.DefWindowProc(hWnd, msg, wParam, lParam); } /// /// Clean up DirectX resources for a monitor /// private void CleanupDirectXResources(int monitorIndex) { try { if (_renderTargetViews.TryGetValue(monitorIndex, out RenderTargetView renderTargetView)) { renderTargetView.Dispose(); _renderTargetViews.Remove(monitorIndex); } if (_vertexBuffers.TryGetValue(monitorIndex, out Buffer vertexBuffer)) { vertexBuffer.Dispose(); _vertexBuffers.Remove(monitorIndex); } if (_inputLayouts.TryGetValue(monitorIndex, out InputLayout inputLayout)) { inputLayout.Dispose(); _inputLayouts.Remove(monitorIndex); } if (_vertexShaders.TryGetValue(monitorIndex, out VertexShader vertexShader)) { vertexShader.Dispose(); _vertexShaders.Remove(monitorIndex); } if (_pixelShaders.TryGetValue(monitorIndex, out PixelShader pixelShader)) { pixelShader.Dispose(); _pixelShaders.Remove(monitorIndex); } if (_swapChains.TryGetValue(monitorIndex, out SwapChain swapChain)) { swapChain.Dispose(); _swapChains.Remove(monitorIndex); } if (_deviceContexts.TryGetValue(monitorIndex, out DeviceContext context)) { context.Dispose(); _deviceContexts.Remove(monitorIndex); } if (_devices.TryGetValue(monitorIndex, out Device device)) { device.Dispose(); _devices.Remove(monitorIndex); } _needsUpdate.Remove(monitorIndex); } catch (Exception ex) { Debug.WriteLine($"CleanupDirectXResources error: {ex.Message}"); } } /// /// Disposes of all resources used by screen overlay /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { _drawingThreadRunning = false; _drawingSignal.Set(); if (_drawingThread != null && _drawingThread.IsAlive) { try { _drawingThread.Join(1000); } catch (Exception ex) { Debug.WriteLine($"Thread join error: {ex.Message}"); } } _drawingSignal.Dispose(); List windowsToDestroy = new List(); foreach (var handle in _overlayWindows.Values) { if (handle != IntPtr.Zero) windowsToDestroy.Add(handle); } foreach (var monitorIndex in _devices.Keys.ToList()) { if (_customRenderables.TryGetValue(monitorIndex, out var renderables)) { foreach (var renderable in renderables.Values) { try { renderable.Dispose(); } catch (Exception ex) { Debug.WriteLine($"Error disposing custom renderable: {ex.Message}"); } } renderables.Clear(); } if (_customShaders.TryGetValue(monitorIndex, out var shaders)) { foreach (var shader in shaders.Values) { try { shader.Dispose(); } catch (Exception ex) { Debug.WriteLine($"Error disposing custom shader: {ex.Message}"); } } shaders.Clear(); } CleanupDirectXResources(monitorIndex); } foreach (var handle in windowsToDestroy) { NativeMethods.DestroyWindow(handle); } _overlayWindows.Clear(); } } /// /// Native methods for window operations /// public static class NativeMethods { public const int GWL_EXSTYLE = -20; public const int WS_EX_LAYERED = 0x80000; public const int WS_EX_TRANSPARENT = 0x20; public const int WS_EX_TOPMOST = 0x8; public const int WS_EX_TOOLWINDOW = 0x80; public const uint WS_POPUP = 0x80000000; public const int SWP_NOMOVE = 0x2; public const int SWP_NOSIZE = 0x1; public const int SWP_NOACTIVATE = 0x10; public const int SWP_SHOWWINDOW = 0x40; public const int SWP_NOOWNERZORDER = 0x0200; public const int HWND_TOPMOST = -1; public const int SWP_NOZORDER = 4; public const int HWND_NOTOPMOST = -2; public const int LWA_COLORKEY = 0x00000001; public const int LWA_ALPHA = 0x00000002; public const int SW_SHOW = 5; public const int SW_SHOWNA = 8; public const int BLACK_BRUSH = 4; public const int NULL_BRUSH = 5; public const uint WM_DESTROY = 0x0002; public const uint WM_PAINT = 0x000F; [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport("user32.dll", SetLastError = true)] public static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport("user32.dll", SetLastError = true)] public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); [DllImport("user32.dll", SetLastError = true)] public static extern bool SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); [DllImport("user32.dll")] public static extern IntPtr DefWindowProc(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr CreateWindowEx(uint dwExStyle, string lpClassName, string lpWindowName, uint dwStyle, int X, int Y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll", SetLastError = true)] public static extern bool RegisterClass(ref WNDCLASS lpWndClass); [DllImport("user32.dll", SetLastError = true)] public static extern bool DestroyWindow(IntPtr hWnd); [DllImport("user32.dll", SetLastError = true)] public static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, uint dwFlags); [DllImport("gdi32.dll", SetLastError = true)] public static extern IntPtr GetStockObject(int fnObject); [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); } /// /// Gets data about the pixel (color etc.) from behind the overlay at specific coordinates /// /// X coordinate on the screen /// Y coordinate on the screen /// Monitor index (defaults to primary monitor) /// Pixel data including color and position /// /// This method captures a single pixel from the screen beneath the overlay. /// It uses GDI+ to take a screenshot of a 1x1 area at the specified coordinates. /// /// Example: /// /// PixelData pixel = overlay.GetPixelCoords(100, 100); /// Console.WriteLine($"Pixel color: R={pixel.Color.R}, G={pixel.Color.G}, B={pixel.Color.B}"); /// Console.WriteLine($"Pixel position: X={pixel.X}, Y={pixel.Y}"); /// /// public PixelData GetPixelCoords(int x, int y, int monitorIndex = 0) { if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) { monitorIndex = 0; } Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; int screenX = bounds.Left + x; int screenY = bounds.Top + y; if (x < 0 || x >= bounds.Width || y < 0 || y >= bounds.Height) { return new PixelData(Color.Black, x, y, monitorIndex); } try { using (Bitmap bitmap = new Bitmap(1, 1, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) { using (Graphics g = Graphics.FromImage(bitmap)) { g.CopyFromScreen(screenX, screenY, 0, 0, new Size(1, 1), CopyPixelOperation.SourceCopy); } Color pixelColor = bitmap.GetPixel(0, 0); return new PixelData(pixelColor, x, y, monitorIndex); } } catch (Exception ex) { Debug.WriteLine($"Error capturing pixel: {ex.Message}"); return new PixelData(Color.Black, x, y, monitorIndex); } } /// /// Gets data about a random pixel from the screen behind the overlay /// /// Monitor index (defaults to primary monitor) /// Pixel data including color and random position /// /// This method selects random coordinates within the specified monitor's bounds /// and returns the pixel data at that location. Useful for creating custom effects. /// /// Example: /// /// PixelData randomPixel = overlay.GetRandomPixel(); /// Console.WriteLine($"Random pixel at X={randomPixel.X}, Y={randomPixel.Y} has color {randomPixel.Color}"); /// /// public PixelData GetRandomPixel(int monitorIndex = 0) { if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) { monitorIndex = 0; } Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; Random random = new Random(); int x = random.Next(0, bounds.Width); int y = random.Next(0, bounds.Height); return GetPixelCoords(x, y, monitorIndex); } /// /// Captures a region of the screen /// /// X coordinate /// Y coordinate /// Width of the region /// Height of the region /// Monitor index /// Bitmap containing the captured region /// /// This method captures a rectangular region of the screen from beneath the overlay and returns it as a Bitmap object. /// Useful for creating effects. /// /// Example: /// /// // Capture a 200x150 region starting at coordinates (50, 50) /// Bitmap screenshot = overlay.CaptureScreenRegion(50, 50, 200, 150); /// /// // Save the screenshot to a file /// screenshot.Save("screenshot.png"); /// /// // Don't forget to dispose when done /// screenshot.Dispose(); /// /// public Bitmap CaptureScreenRegion(int x, int y, int width, int height, int monitorIndex = 0) { if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) { monitorIndex = 0; } Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; int screenX = bounds.Left + x; int screenY = bounds.Top + y; width = Math.Max(1, width); height = Math.Max(1, height); if (x < 0) { width += x; x = 0; } if (y < 0) { height += y; y = 0; } if (x + width > bounds.Width) width = bounds.Width - x; if (y + height > bounds.Height) height = bounds.Height - y; if (width <= 0 || height <= 0) { return new Bitmap(1, 1); } try { Bitmap bitmap = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); using (Graphics g = Graphics.FromImage(bitmap)) { g.CopyFromScreen(screenX, screenY, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy); } return bitmap; } catch (Exception ex) { Debug.WriteLine($"Error capturing screen region: {ex.Message}"); return new Bitmap(1, 1); } } private class Cube3DData { public int Id { get; set; } public Vector3 Position { get; set; } public Vector3 Rotation { get; set; } public Vector3 Scale { get; set; } public Color4 FrontColor { get; set; } public Color4 BackColor { get; set; } public Color4 TopColor { get; set; } public Color4 BottomColor { get; set; } public Color4 LeftColor { get; set; } public Color4 RightColor { get; set; } public bool IsMoving { get; set; } public Vector3 TargetPosition { get; set; } public float MoveSpeed { get; set; } public bool IsRotating { get; set; } public Vector3 TargetRotation { get; set; } public float RotationSpeed { get; set; } public Buffer VertexBuffer { get; set; } public Buffer IndexBuffer { get; set; } public int VertexCount { get; set; } public int IndexCount { get; set; } public Matrix WorldMatrix { get; private set; } public Cube3DData(int id, Color4 frontColor, Color4 backColor, Color4 topColor, Color4 bottomColor, Color4 leftColor, Color4 rightColor, float x, float y, float z, float width, float height, float depth = 0) { Id = id; Position = new Vector3(x, y, z); Rotation = Vector3.Zero; Scale = new Vector3(width, height, depth > 0 ? depth : width); FrontColor = frontColor; BackColor = backColor; TopColor = topColor; BottomColor = bottomColor; LeftColor = leftColor; RightColor = rightColor; IsMoving = false; IsRotating = false; TargetPosition = Position; TargetRotation = Rotation; MoveSpeed = 5.0f; RotationSpeed = 3.0f; UpdateWorldMatrix(); } public void UpdateWorldMatrix() { Matrix rotationX = Matrix.RotationX(Rotation.X); Matrix rotationY = Matrix.RotationY(Rotation.Y); Matrix rotationZ = Matrix.RotationZ(Rotation.Z); Matrix rotationMatrix = rotationX * rotationY * rotationZ; Matrix scaleMatrix = Matrix.Scaling(Scale); Matrix translationMatrix = Matrix.Translation(Position); WorldMatrix = scaleMatrix * rotationMatrix * translationMatrix; } } private void UpdateAnimatedCubes() { float deltaTime = 0.016f; foreach (var monitorCubes in _cubes) { bool cubeUpdated = false; foreach (var cube in monitorCubes.Value.Values.ToList()) { bool updated = false; if (cube.IsMoving) { Vector3 direction = cube.TargetPosition - cube.Position; float distance = direction.Length(); if (distance > 0.01f) { direction.Normalize(); float moveAmount = cube.MoveSpeed * deltaTime; if (distance <= moveAmount) { cube.Position = cube.TargetPosition; cube.IsMoving = false; } else { cube.Position += direction * moveAmount; } updated = true; } else { cube.IsMoving = false; } } if (cube.IsRotating) { Vector3 rotationDelta = cube.TargetRotation - cube.Rotation; float rotationDistance = rotationDelta.Length(); if (rotationDistance > 0.01f) { float rotationAmount = cube.RotationSpeed * deltaTime; if (rotationDistance <= rotationAmount) { cube.Rotation = cube.TargetRotation; cube.IsRotating = false; } else { Vector3 rotationDir = new Vector3( rotationDelta.X != 0 ? Math.Sign(rotationDelta.X) : 0, rotationDelta.Y != 0 ? Math.Sign(rotationDelta.Y) : 0, rotationDelta.Z != 0 ? Math.Sign(rotationDelta.Z) : 0 ); cube.Rotation += rotationDir * rotationAmount; } updated = true; } else { cube.IsRotating = false; } } if (updated) { cube.UpdateWorldMatrix(); cubeUpdated = true; } } if (cubeUpdated) { _needsUpdate[monitorCubes.Key] = true; } } } /// /// Renders a 3D cube on screen with the specified parameters /// /// Unique identifier for the cube /// Color for the front face /// Color for the back face /// Color for the top face /// Color for the bottom face /// Color for the left face /// Color for the right face /// X position in screen coordinates /// Y position in screen coordinates /// Z position (depth) /// Width of the cube /// Height of the cube /// Monitor index to display the cube on /// /// Creates a 3D cube with customizable colors for each face. The cube is positioned at the specified coordinates /// with the given dimensions. Each cube has a unique ID that can be used to manipulate it later. /// /// Example: /// /// // Create a red/blue/green/yellow/purple/orange cube at position (100, 100) with size 50x50 /// overlay.Render3DCube(1, Color.Red.ToArgb(), Color.Blue.ToArgb(), Color.Green.ToArgb(), /// Color.Yellow.ToArgb(), Color.Purple.ToArgb(), Color.Orange.ToArgb(), /// 100, 100, 0, 50, 50); /// /// public void Render3DCube(int id, int frontColor, int backColor, int topColor, int bottomColor, int leftColor, int rightColor, float x, float y, float z, float width, float height, int monitorIndex = 0) { QueueDrawingAction(() => { try { if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) { monitorIndex = 0; } Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; ShowDrawingOverlay(monitorIndex, bounds); if (!_vertex3DShaders.ContainsKey(monitorIndex)) { Setup3DResources(monitorIndex); } if (!_cubes.ContainsKey(monitorIndex)) { _cubes[monitorIndex] = new Dictionary(); } if (_cubes[monitorIndex].ContainsKey(id)) { Remove3DCube(id, monitorIndex); } float ndcX = (x / (float)bounds.Width) * 2.0f - 1.0f; float ndcY = 1.0f - (y / (float)bounds.Height) * 2.0f; float ndcWidth = width / (float)bounds.Width * 2.0f; float ndcHeight = height / (float)bounds.Height * 2.0f; Color4 front = ARGBToColor4(frontColor); Color4 back = ARGBToColor4(backColor); Color4 top = ARGBToColor4(topColor); Color4 bottom = ARGBToColor4(bottomColor); Color4 left = ARGBToColor4(leftColor); Color4 right = ARGBToColor4(rightColor); var cube = new Cube3DData(id, front, back, top, bottom, left, right, ndcX, ndcY, z, ndcWidth, ndcHeight); CreateCubeMesh(_devices[monitorIndex], cube); _cubes[monitorIndex][id] = cube; _needsUpdate[monitorIndex] = true; } catch (Exception ex) { Debug.WriteLine($"Render3DCube error: {ex.Message}"); } }); } /// /// Moves a 3D cube to the specified position /// /// ID of the cube to move /// If true, animate the movement; otherwise move immediately /// X position in screen coordinates /// Y position in screen coordinates /// Z position (depth) /// Monitor index the cube is on /// /// Moves an existing 3D cube to a new position. If smooth is true, the cube will /// animate smoothly to the new position over several frames. If false, the cube /// will instantly teleport to the new position. /// /// Example: /// /// // Move cube with ID 1 smoothly to position (200, 200) /// overlay.Move3DCube(1, true, 200, 200, 0); /// /// // Move cube with ID 1 instantly to position (300, 300) /// overlay.Move3DCube(1, false, 300, 300, 0); /// /// public void Move3DCube(int id, bool smooth, float x, float y, float z, int monitorIndex = 0) { QueueDrawingAction(() => { try { if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) { monitorIndex = 0; } if (!_cubes.ContainsKey(monitorIndex) || !_cubes[monitorIndex].ContainsKey(id)) { Debug.WriteLine($"Cube with ID {id} not found on monitor {monitorIndex}"); return; } var cube = _cubes[monitorIndex][id]; Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; float ndcX = (x / (float)bounds.Width) * 2.0f - 1.0f; float ndcY = 1.0f - (y / (float)bounds.Height) * 2.0f; if (smooth) { cube.TargetPosition = new Vector3(ndcX, ndcY, z); cube.IsMoving = true; } else { cube.Position = new Vector3(ndcX, ndcY, z); cube.UpdateWorldMatrix(); } _needsUpdate[monitorIndex] = true; } catch (Exception ex) { Debug.WriteLine($"Move3DCube error: {ex.Message}"); } }); } /// /// Rotates a 3D cube to the specified orientation /// /// ID of the cube to rotate /// If true, animate the rotation; otherwise rotate immediately /// X rotation in radians /// Y rotation in radians /// Z rotation in radians /// Monitor index the cube is on /// /// Rotates an existing 3D cube to a new orientation. If smooth is true, the cube will /// animate smoothly to the new orientation over several frames. If false, the cube /// will instantly rotate to the new orientation. /// /// Example: /// /// // Rotate cube with ID 1 smoothly to a new orientation /// overlay.Rotate3DCube(1, true, 0.5f, 1.0f, 0.25f); /// /// // Rotate cube with ID 1 instantly to a specific orientation /// overlay.Rotate3DCube(1, false, 0, (float)Math.PI/2, 0); /// /// public void Rotate3DCube(int id, bool smooth, float x, float y, float z, int monitorIndex = 0) { QueueDrawingAction(() => { try { if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) { monitorIndex = 0; } if (!_cubes.ContainsKey(monitorIndex) || !_cubes[monitorIndex].ContainsKey(id)) { Debug.WriteLine($"Cube with ID {id} not found on monitor {monitorIndex}"); return; } var cube = _cubes[monitorIndex][id]; if (smooth) { cube.TargetRotation = new Vector3(x, y, z); cube.IsRotating = true; } else { cube.Rotation = new Vector3(x, y, z); cube.UpdateWorldMatrix(); } _needsUpdate[monitorIndex] = true; } catch (Exception ex) { Debug.WriteLine($"Rotate3DCube error: {ex.Message}"); } }); } /// /// Removes a 3D cube from the overlay /// /// ID of the cube to remove /// Monitor index the cube is on /// /// Completely removes a 3D cube from the overlay and disposes its resources. /// /// Example: /// /// // Remove cube with ID 1 /// overlay.Remove3DCube(1); /// /// public void Remove3DCube(int id, int monitorIndex = 0) { QueueDrawingAction(() => { try { if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) { monitorIndex = 0; } if (!_cubes.ContainsKey(monitorIndex) || !_cubes[monitorIndex].ContainsKey(id)) { return; } var cube = _cubes[monitorIndex][id]; if (cube.VertexBuffer != null) { cube.VertexBuffer.Dispose(); } if (cube.IndexBuffer != null) { cube.IndexBuffer.Dispose(); } _cubes[monitorIndex].Remove(id); _needsUpdate[monitorIndex] = true; } catch (Exception ex) { Debug.WriteLine($"Remove3DCube error: {ex.Message}"); } }); } /// /// Converts an ARGB color integer to a Direct3D Color4 /// private Color4 ARGBToColor4(int colorArgb) { Color argbColor = Color.FromArgb(colorArgb); return new Color4( argbColor.R / 255.0f, argbColor.G / 255.0f, argbColor.B / 255.0f, argbColor.A / 255.0f ); } /// /// Interface for custom shader implementations /// /// /// Use this interface to create custom shaders that can be registered with the ScreenOverlay. /// Custom shaders allow you to apply custom rendering effects to the overlay. /// /// Implementation example: /// /// public class MyCustomShader : ICustomShader /// { /// private string _id = "MyShader"; /// private Device _device; /// private VertexShader _vertexShader; /// private PixelShader _pixelShader; /// private InputLayout _inputLayout; /// /// public string ShaderId => _id; /// /// public bool Initialize(Device device) /// { /// _device = device; /// /// // Compile shaders /// using (var vertexShaderBytecode = SharpDX.D3DCompiler.ShaderBytecode.Compile( /// vertexShaderCode, "main", "vs_4_0", SharpDX.D3DCompiler.ShaderFlags.Debug)) /// { /// _vertexShader = new VertexShader(device, vertexShaderBytecode); /// /// // Create input layout /// _inputLayout = new InputLayout(device, vertexShaderBytecode, inputElements); /// } /// /// using (var pixelShaderBytecode = SharpDX.D3DCompiler.ShaderBytecode.Compile( /// pixelShaderCode, "main", "ps_4_0", SharpDX.D3DCompiler.ShaderFlags.Debug)) /// { /// _pixelShader = new PixelShader(device, pixelShaderBytecode); /// } /// /// return true; /// } /// /// public void Apply(DeviceContext context) /// { /// context.VertexShader.Set(_vertexShader); /// context.PixelShader.Set(_pixelShader); /// context.InputAssembler.InputLayout = _inputLayout; /// } /// /// public void Dispose() /// { /// _vertexShader?.Dispose(); /// _pixelShader?.Dispose(); /// _inputLayout?.Dispose(); /// } /// } /// /// public interface ICustomShader : IDisposable { /// /// Initializes the shader with a device /// /// The Direct3D device /// True if initialization succeeded bool Initialize(Device device); /// /// Applies the shader to the rendering pipeline /// /// The Direct3D device context void Apply(DeviceContext context); /// /// Gets the unique identifier for this shader /// string ShaderId { get; } } /// /// Interface for custom renderable objects /// /// /// Use this interface to create custom 3D objects that can be rendered by the ScreenOverlay. /// This allows you to extend the ScreenOverlay with your own rendering logic. /// /// Implementation example: /// /// public class MyCube : ICustomRenderable /// { /// private int _id; /// private int _monitorIndex; /// private Vector3 _position; /// private Vector3 _rotation; /// private Vector3 _scale; /// private Color4 _color; /// /// private Buffer _vertexBuffer; /// private Buffer _indexBuffer; /// private Buffer _constantBuffer; /// private int _vertexCount; /// private int _indexCount; /// /// public MyCube(int id, int monitorIndex, Vector3 position, Color4 color) /// { /// _id = id; /// _monitorIndex = monitorIndex; /// _position = position; /// _color = color; /// _rotation = Vector3.Zero; /// _scale = new Vector3(1, 1, 1); /// } /// /// public int RenderableId => _id; /// public bool NeedsUpdate { get; set; } = true; /// public int MonitorIndex => _monitorIndex; /// /// public bool Initialize(Device device) /// { /// // Create vertex and index buffers /// // Create geometry /// // Create constant buffer for transformations /// return true; /// } /// /// public void Render(DeviceContext context, Matrix viewMatrix, Matrix projectionMatrix) /// { /// // Create world matrix from position, rotation, scale /// Matrix worldMatrix = Matrix.Scaling(_scale) * /// Matrix.RotationYawPitchRoll(_rotation.Y, _rotation.X, _rotation.Z) * /// Matrix.Translation(_position); /// /// // Update constant buffer with matrices /// // Set vertex and index buffers /// // Draw the object /// context.DrawIndexed(_indexCount, 0, 0); /// } /// /// public void Dispose() /// { /// _vertexBuffer?.Dispose(); /// _indexBuffer?.Dispose(); /// _constantBuffer?.Dispose(); /// } /// } /// /// /// Register your renderable with the ScreenOverlay: /// /// var cube = new MyCube(1, 0, new Vector3(0, 0, 0), new Color4(1, 0, 0, 1)); /// overlay.RegisterCustomRenderable(cube); /// /// public interface ICustomRenderable : IDisposable { /// /// Initializes resources for the renderable object /// /// The Direct3D device /// True if initialization succeeded bool Initialize(Device device); /// /// Renders the object using the specified context /// /// The Direct3D device context /// The view matrix /// The projection matrix void Render(DeviceContext context, Matrix viewMatrix, Matrix projectionMatrix); /// /// Gets the unique identifier for this renderable /// int RenderableId { get; } /// /// Gets or sets whether this object needs to be updated /// bool NeedsUpdate { get; set; } /// /// Gets the monitor index this renderable belongs to /// int MonitorIndex { get; } } /// /// Registers a custom shader for use with this overlay /// /// The custom shader to register /// The monitor index to register the shader with /// True if registration succeeded /// /// Register a custom shader that implements the ICustomShader interface. /// Custom shaders allow you to apply custom rendering effects to the overlay. /// /// Example: /// /// // Create and register a custom shader /// var shader = new MyCustomShader(); /// bool success = overlay.RegisterCustomShader(shader); /// /// public bool RegisterCustomShader(ICustomShader shader, int monitorIndex = 0) { return QueueDrawingActionWithResult(() => { try { if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) { monitorIndex = 0; } Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; ShowDrawingOverlay(monitorIndex, bounds); if (!_customShaders.ContainsKey(monitorIndex)) { _customShaders[monitorIndex] = new Dictionary(); } if (_customShaders[monitorIndex].ContainsKey(shader.ShaderId)) { _customShaders[monitorIndex][shader.ShaderId].Dispose(); } if (!shader.Initialize(_devices[monitorIndex])) { Debug.WriteLine($"Failed to initialize custom shader {shader.ShaderId}"); return false; } _customShaders[monitorIndex][shader.ShaderId] = shader; return true; } catch (Exception ex) { Debug.WriteLine($"RegisterCustomShader error: {ex.Message}"); return false; } }); } /// /// Unregisters a custom shader /// /// The ID of the shader to unregister /// The monitor index the shader is registered with /// True if unregistration succeeded /// /// Removes a previously registered custom shader and disposes its resources. /// /// Example: /// /// // Unregister a shader with ID "MyShader" /// overlay.UnregisterCustomShader("MyShader"); /// /// public bool UnregisterCustomShader(string shaderId, int monitorIndex = 0) { return QueueDrawingActionWithResult(() => { try { if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) { monitorIndex = 0; } if (!_customShaders.ContainsKey(monitorIndex) || !_customShaders[monitorIndex].ContainsKey(shaderId)) { return false; } _customShaders[monitorIndex][shaderId].Dispose(); _customShaders[monitorIndex].Remove(shaderId); return true; } catch (Exception ex) { Debug.WriteLine($"UnregisterCustomShader error: {ex.Message}"); return false; } }); } /// /// Registers a custom renderable object for rendering with this overlay /// /// The custom renderable to register /// True if registration succeeded /// /// Register a custom renderable object that implements the ICustomRenderable interface. /// This allows you to create your own 3D objects and have them rendered by the overlay. /// /// Example: /// /// // Create and register a custom 3D object /// var mySphere = new CustomSphere(1, 0, new Vector3(0, 0, 0), 1.0f); /// bool success = overlay.RegisterCustomRenderable(mySphere); /// /// public bool RegisterCustomRenderable(ICustomRenderable renderable) { return QueueDrawingActionWithResult(() => { try { int monitorIndex = renderable.MonitorIndex; if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) { monitorIndex = 0; } Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; ShowDrawingOverlay(monitorIndex, bounds); if (!_customRenderables.ContainsKey(monitorIndex)) { _customRenderables[monitorIndex] = new Dictionary(); } if (_customRenderables[monitorIndex].ContainsKey(renderable.RenderableId)) { _customRenderables[monitorIndex][renderable.RenderableId].Dispose(); } if (!renderable.Initialize(_devices[monitorIndex])) { Debug.WriteLine($"Failed to initialize custom renderable {renderable.RenderableId}"); return false; } _customRenderables[monitorIndex][renderable.RenderableId] = renderable; _needsUpdate[monitorIndex] = true; return true; } catch (Exception ex) { Debug.WriteLine($"RegisterCustomRenderable error: {ex.Message}"); return false; } }); } /// /// Unregisters a custom renderable /// /// The ID of the renderable to unregister /// The monitor index the renderable is registered with /// True if unregistration succeeded /// /// Removes a previously registered custom renderable object and disposes its resources. /// /// Example: /// /// // Unregister a renderable with ID 1 /// overlay.UnregisterCustomRenderable(1); /// /// public bool UnregisterCustomRenderable(int renderableId, int monitorIndex = 0) { return QueueDrawingActionWithResult(() => { try { if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) { monitorIndex = 0; } if (!_customRenderables.ContainsKey(monitorIndex) || !_customRenderables[monitorIndex].ContainsKey(renderableId)) { return false; } _customRenderables[monitorIndex][renderableId].Dispose(); _customRenderables[monitorIndex].Remove(renderableId); _needsUpdate[monitorIndex] = true; return true; } catch (Exception ex) { Debug.WriteLine($"UnregisterCustomRenderable error: {ex.Message}"); return false; } }); } /// /// Updates a custom renderable to trigger a re-render /// /// The ID of the renderable to update /// The monitor index the renderable is on /// /// Marks a custom renderable as needing an update, which will trigger a re-render /// on the next frame. Call this after modifying properties of your custom renderable /// to ensure the changes are displayed. /// /// Example: /// /// // Update position of custom sphere in our code /// mySphere.Position = new Vector3(100, 100, 0); /// /// // Tell the overlay to update the rendering of this object /// overlay.UpdateCustomRenderable(mySphere.RenderableId); /// /// public void UpdateCustomRenderable(int renderableId, int monitorIndex = 0) { QueueDrawingAction(() => { try { if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) { monitorIndex = 0; } if (!_customRenderables.ContainsKey(monitorIndex) || !_customRenderables[monitorIndex].ContainsKey(renderableId)) { return; } _customRenderables[monitorIndex][renderableId].NeedsUpdate = true; _needsUpdate[monitorIndex] = true; } catch (Exception ex) { Debug.WriteLine($"UpdateCustomRenderable error: {ex.Message}"); } }); } /// /// Gets a custom shader by ID /// /// The ID of the shader to retrieve /// The monitor index the shader is registered with /// The custom shader, or null if not found /// /// Retrieves a previously registered custom shader by its ID. /// /// Example: /// /// // Get a registered shader /// ICustomShader shader = overlay.GetCustomShader("MyShader"); /// if (shader != null) /// { /// // Use shader... /// } /// /// public ICustomShader GetCustomShader(string shaderId, int monitorIndex = 0) { try { if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) { monitorIndex = 0; } if (!_customShaders.ContainsKey(monitorIndex) || !_customShaders[monitorIndex].ContainsKey(shaderId)) { return null; } return _customShaders[monitorIndex][shaderId]; } catch (Exception ex) { Debug.WriteLine($"GetCustomShader error: {ex.Message}"); return null; } } /// /// Queues a drawing action that returns a result /// private T QueueDrawingActionWithResult(Func action) { if (action == null) return default; ManualResetEvent waitEvent = new ManualResetEvent(false); T result = default; Exception exception = null; QueueDrawingAction(() => { try { result = action(); } catch (Exception ex) { exception = ex; } finally { waitEvent.Set(); } }); waitEvent.WaitOne(); if (exception != null) { Debug.WriteLine($"QueueDrawingActionWithResult error: {exception.Message}"); } return result; } /// /// Gets the Direct3D device for a specific monitor /// /// The monitor index /// The Direct3D device, or null if not available /// /// Provides access to the Direct3D device for a specific monitor. /// This is useful for advanced rendering scenarios or when creating /// custom renderables that need direct access to the device. /// /// Example: /// /// // Get the device for the primary monitor /// Device device = overlay.GetDeviceForMonitor(0); /// if (device != null) /// { /// // Use device for advanced DirectX operations /// } /// /// public Device GetDeviceForMonitor(int monitorIndex = 0) { return QueueDrawingActionWithResult(() => { try { if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) { monitorIndex = 0; } if (!_devices.ContainsKey(monitorIndex)) { Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; ShowDrawingOverlay(monitorIndex, bounds); } if (_devices.TryGetValue(monitorIndex, out Device device)) { return device; } return null; } catch (Exception ex) { Debug.WriteLine($"GetDeviceForMonitor error: {ex.Message}"); return null; } }); } } }