Files
i2p 773d05f8f1
Pulsar .NET 9.0 Windows Release / build (push) Waiting to run
Mirror to Codeberg and Gitea / mirror (push) Waiting to run
initial commit
2026-08-27 10:57:58 -06:00

2549 lines
112 KiB
C#

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
{
/// <summary>
/// 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.
/// </summary>
[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);
/// <summary>
/// For creating transparent DX overlays on the screen
/// </summary>
public class ScreenOverlay : IDisposable
{
private Dictionary<int, IntPtr> _overlayWindows = new Dictionary<int, IntPtr>();
private Dictionary<int, Device> _devices = new Dictionary<int, Device>();
private Dictionary<int, DeviceContext> _deviceContexts = new Dictionary<int, DeviceContext>();
private Dictionary<int, SwapChain> _swapChains = new Dictionary<int, SwapChain>();
private Dictionary<int, RenderTargetView> _renderTargetViews = new Dictionary<int, RenderTargetView>();
private Dictionary<int, ShaderResourceView> _shaderResourceViews = new Dictionary<int, ShaderResourceView>();
private Dictionary<int, Texture2D> _renderTextures = new Dictionary<int, Texture2D>();
private Dictionary<int, bool> _needsUpdate = new Dictionary<int, bool>();
private Dictionary<int, VertexShader> _vertexShaders = new Dictionary<int, VertexShader>();
private Dictionary<int, PixelShader> _pixelShaders = new Dictionary<int, PixelShader>();
private Dictionary<int, InputLayout> _inputLayouts = new Dictionary<int, InputLayout>();
private Dictionary<int, Buffer> _vertexBuffers = new Dictionary<int, Buffer>();
private Dictionary<int, VertexShader> _vertex3DShaders = new Dictionary<int, VertexShader>();
private Dictionary<int, PixelShader> _pixel3DShaders = new Dictionary<int, PixelShader>();
private Dictionary<int, InputLayout> _input3DLayouts = new Dictionary<int, InputLayout>();
private Dictionary<int, Buffer> _matrixBuffers = new Dictionary<int, Buffer>();
private Dictionary<int, Dictionary<int, Cube3DData>> _cubes = new Dictionary<int, Dictionary<int, Cube3DData>>();
private WndProcDelegate _wndProcDelegate;
private bool _delegateInit = false;
private ConcurrentQueue<Action> _drawingQueue = new ConcurrentQueue<Action>();
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<VertexPositionColor>();
}
[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<VertexPosition3DColor>();
}
private class MonitorDrawingData
{
public List<VertexPositionColor> Points { get; private set; } = new List<VertexPositionColor>();
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<int, MonitorDrawingData> _drawingData = new Dictionary<int, MonitorDrawingData>();
private Dictionary<int, Buffer> _indexBuffers = new Dictionary<int, Buffer>();
private Dictionary<int, int> _bufferSizes = new Dictionary<int, int>();
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<int, Dictionary<string, ICustomShader>> _customShaders = new Dictionary<int, Dictionary<string, ICustomShader>>();
private Dictionary<int, Dictionary<int, ICustomRenderable>> _customRenderables = new Dictionary<int, Dictionary<int, ICustomRenderable>>();
/// <summary>
/// Creates a new ScreenOverlay instance
/// </summary>
public ScreenOverlay()
{
drawThreadRunning();
}
/// <summary>
/// Checks if drawing thread is running
/// </summary>
private void drawThreadRunning()
{
if (!_drawingThreadRunning)
{
lock (_drawingQueue)
{
if (!_drawingThreadRunning)
{
_drawingThreadRunning = true;
_drawingThread = new Thread(DrawingThreadProc);
_drawingThread.IsBackground = true;
_drawingThread.Start();
}
}
}
}
/// <summary>
/// Thread proc for handling drawing operations asynchronously
/// </summary>
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;
}
}
/// <summary>
/// Queues a drawing operation to be async executed
/// </summary>
public void QueueDrawingAction(Action drawAction)
{
if (drawAction == null)
return;
drawThreadRunning();
_drawingQueue.Enqueue(drawAction);
_drawingSignal.Set();
}
/// <summary>
/// Draws a point on a monitor. The strokeWidth parameter controls the size of the point.
/// </summary>
/// <param name="prevX">Previous X coordinate (ignored in Direct3D implementation which only x,y are used)</param>
/// <param name="prevY">Previous Y coordinate (ignored in Direct3D implementation which only x,y are used)</param>
/// <param name="x">X coordinate to draw at</param>
/// <param name="y">Y coordinate to draw at</param>
/// <param name="strokeWidth">Width/size of the point</param>
/// <param name="colorArgb">ARGB color value</param>
/// <param name="monitorIndex">Monitor index to draw on</param>
public void Draw(int prevX, int prevY, int x, int y, int strokeWidth, int colorArgb, int monitorIndex)
{
QueueDrawingAction(() => DrawPoint(monitorIndex, x, y, colorArgb, strokeWidth));
}
/// <summary>
/// Erases at the specified coordinates on a monitor by removing any points in the radius determined by strokeWidth
/// </summary>
/// <param name="prevX">Previous X coordinate (ignored in Direct3D implementation which only x,y are used)</param>
/// <param name="prevY">Previous Y coordinate (ignored in Direct3D implementation which only x,y are used)</param>
/// <param name="x">X coordinate to erase at</param>
/// <param name="y">Y coordinate to erase at</param>
/// <param name="strokeWidth">Width of the eraser (radius)</param>
/// <param name="monitorIndex">Monitor index to erase on</param>
public void DrawEraser(int prevX, int prevY, int x, int y, int strokeWidth, int monitorIndex)
{
QueueDrawingAction(() => Erase(monitorIndex, x, y, strokeWidth));
}
/// <summary>
/// Draws a point on the screen at the specified coordinates
/// </summary>
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}");
}
}
/// <summary>
/// Erases at the specified coordinates
/// </summary>
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<VertexPositionColor> remainingPoints = new List<VertexPositionColor>(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}");
}
}
/// <summary>
/// Clears all drawings on a monitor
/// </summary>
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}");
}
});
}
/// <summary>
/// Creates the overlay window and DirectX resources
/// </summary>
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);
}
/// <summary>
/// Create DirectX resources for a specific monitor overlay including swap chain, buffers, and shaders
/// </summary>
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<Texture2D>(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}");
}
}
/// <summary>
/// Sets up the 3D resources for the overlay
/// </summary>
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<Matrix>() * 3,
BindFlags = BindFlags.ConstantBuffer,
CpuAccessFlags = CpuAccessFlags.Write,
OptionFlags = ResourceOptionFlags.None,
StructureByteStride = 0
};
_matrixBuffers[monitorIndex] = new Buffer(device, matrixBufferDesc);
_cubes[monitorIndex] = new Dictionary<int, Cube3DData>();
}
catch (Exception ex)
{
Debug.WriteLine($"Error setting up 3D resources: {ex.Message}");
}
}
}
/// <summary>
/// Creates a cube mesh with the specified colors for each face
/// </summary>
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;
}
/// <summary>
/// Compile vertex and pixel shaders at runtime
/// </summary>
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;
}
}
/// <summary>
/// Update all active overlays
/// </summary>
private void UpdateActiveOverlays()
{
UpdateAnimatedCubes();
HashSet<int> monitorsToUpdate = new HashSet<int>();
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}");
}
}
}
/// <summary>
/// Renders the overlay for a monitor
/// </summary>
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<int, Cube3DData> cubes) && cubes.Count > 0)
{
Render3DCubes(monitorIndex, context, cubes.Values.ToList());
hasContent = true;
}
if (_customRenderables.TryGetValue(monitorIndex, out Dictionary<int, ICustomRenderable> 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<int, Cube3DData> cubes) && cubes.Count > 0)
{
Render3DCubes(monitorIndex, context, cubes.Values.ToList());
}
if (_customRenderables.TryGetValue(monitorIndex, out Dictionary<int, ICustomRenderable> 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}");
}
}
/// <summary>
/// Renders the 3D cubes for a monitor
/// </summary>
private void Render3DCubes(int monitorIndex, DeviceContext context, List<Cube3DData> 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}");
}
}
/// <summary>
/// Renders custom renderables for a monitor
/// </summary>
private void RenderCustomRenderables(int monitorIndex, DeviceContext context, List<ICustomRenderable> 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<string, ICustomShader> 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<int>();
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);
}
/// <summary>
/// Clean up DirectX resources for a monitor
/// </summary>
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}");
}
}
/// <summary>
/// Disposes of all resources used by screen overlay
/// </summary>
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<IntPtr> windowsToDestroy = new List<IntPtr>();
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();
}
}
/// <summary>
/// Native methods for window operations
/// </summary>
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);
}
/// <summary>
/// Gets data about the pixel (color etc.) from behind the overlay at specific coordinates
/// </summary>
/// <param name="x">X coordinate on the screen</param>
/// <param name="y">Y coordinate on the screen</param>
/// <param name="monitorIndex">Monitor index (defaults to primary monitor)</param>
/// <returns>Pixel data including color and position</returns>
/// <remarks>
/// 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:
/// <code>
/// 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}");
/// </code>
/// </remarks>
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);
}
}
/// <summary>
/// Gets data about a random pixel from the screen behind the overlay
/// </summary>
/// <param name="monitorIndex">Monitor index (defaults to primary monitor)</param>
/// <returns>Pixel data including color and random position</returns>
/// <remarks>
/// 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:
/// <code>
/// PixelData randomPixel = overlay.GetRandomPixel();
/// Console.WriteLine($"Random pixel at X={randomPixel.X}, Y={randomPixel.Y} has color {randomPixel.Color}");
/// </code>
/// </remarks>
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);
}
/// <summary>
/// Captures a region of the screen
/// </summary>
/// <param name="x">X coordinate</param>
/// <param name="y">Y coordinate</param>
/// <param name="width">Width of the region</param>
/// <param name="height">Height of the region</param>
/// <param name="monitorIndex">Monitor index</param>
/// <returns>Bitmap containing the captured region</returns>
/// <remarks>
/// 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:
/// <code>
/// // 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();
/// </code>
/// </remarks>
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;
}
}
}
/// <summary>
/// Renders a 3D cube on screen with the specified parameters
/// </summary>
/// <param name="id">Unique identifier for the cube</param>
/// <param name="frontColor">Color for the front face</param>
/// <param name="backColor">Color for the back face</param>
/// <param name="topColor">Color for the top face</param>
/// <param name="bottomColor">Color for the bottom face</param>
/// <param name="leftColor">Color for the left face</param>
/// <param name="rightColor">Color for the right face</param>
/// <param name="x">X position in screen coordinates</param>
/// <param name="y">Y position in screen coordinates</param>
/// <param name="z">Z position (depth)</param>
/// <param name="width">Width of the cube</param>
/// <param name="height">Height of the cube</param>
/// <param name="monitorIndex">Monitor index to display the cube on</param>
/// <remarks>
/// 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:
/// <code>
/// // 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);
/// </code>
/// </remarks>
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<int, Cube3DData>();
}
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}");
}
});
}
/// <summary>
/// Moves a 3D cube to the specified position
/// </summary>
/// <param name="id">ID of the cube to move</param>
/// <param name="smooth">If true, animate the movement; otherwise move immediately</param>
/// <param name="x">X position in screen coordinates</param>
/// <param name="y">Y position in screen coordinates</param>
/// <param name="z">Z position (depth)</param>
/// <param name="monitorIndex">Monitor index the cube is on</param>
/// <remarks>
/// 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:
/// <code>
/// // 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);
/// </code>
/// </remarks>
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}");
}
});
}
/// <summary>
/// Rotates a 3D cube to the specified orientation
/// </summary>
/// <param name="id">ID of the cube to rotate</param>
/// <param name="smooth">If true, animate the rotation; otherwise rotate immediately</param>
/// <param name="x">X rotation in radians</param>
/// <param name="y">Y rotation in radians</param>
/// <param name="z">Z rotation in radians</param>
/// <param name="monitorIndex">Monitor index the cube is on</param>
/// <remarks>
/// 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:
/// <code>
/// // 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);
/// </code>
/// </remarks>
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}");
}
});
}
/// <summary>
/// Removes a 3D cube from the overlay
/// </summary>
/// <param name="id">ID of the cube to remove</param>
/// <param name="monitorIndex">Monitor index the cube is on</param>
/// <remarks>
/// Completely removes a 3D cube from the overlay and disposes its resources.
///
/// Example:
/// <code>
/// // Remove cube with ID 1
/// overlay.Remove3DCube(1);
/// </code>
/// </remarks>
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}");
}
});
}
/// <summary>
/// Converts an ARGB color integer to a Direct3D Color4
/// </summary>
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
);
}
/// <summary>
/// Interface for custom shader implementations
/// </summary>
/// <remarks>
/// 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:
/// <code>
/// 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();
/// }
/// }
/// </code>
/// </remarks>
public interface ICustomShader : IDisposable
{
/// <summary>
/// Initializes the shader with a device
/// </summary>
/// <param name="device">The Direct3D device</param>
/// <returns>True if initialization succeeded</returns>
bool Initialize(Device device);
/// <summary>
/// Applies the shader to the rendering pipeline
/// </summary>
/// <param name="context">The Direct3D device context</param>
void Apply(DeviceContext context);
/// <summary>
/// Gets the unique identifier for this shader
/// </summary>
string ShaderId { get; }
}
/// <summary>
/// Interface for custom renderable objects
/// </summary>
/// <remarks>
/// 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:
/// <code>
/// 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();
/// }
/// }
/// </code>
///
/// Register your renderable with the ScreenOverlay:
/// <code>
/// var cube = new MyCube(1, 0, new Vector3(0, 0, 0), new Color4(1, 0, 0, 1));
/// overlay.RegisterCustomRenderable(cube);
/// </code>
/// </remarks>
public interface ICustomRenderable : IDisposable
{
/// <summary>
/// Initializes resources for the renderable object
/// </summary>
/// <param name="device">The Direct3D device</param>
/// <returns>True if initialization succeeded</returns>
bool Initialize(Device device);
/// <summary>
/// Renders the object using the specified context
/// </summary>
/// <param name="context">The Direct3D device context</param>
/// <param name="viewMatrix">The view matrix</param>
/// <param name="projectionMatrix">The projection matrix</param>
void Render(DeviceContext context, Matrix viewMatrix, Matrix projectionMatrix);
/// <summary>
/// Gets the unique identifier for this renderable
/// </summary>
int RenderableId { get; }
/// <summary>
/// Gets or sets whether this object needs to be updated
/// </summary>
bool NeedsUpdate { get; set; }
/// <summary>
/// Gets the monitor index this renderable belongs to
/// </summary>
int MonitorIndex { get; }
}
/// <summary>
/// Registers a custom shader for use with this overlay
/// </summary>
/// <param name="shader">The custom shader to register</param>
/// <param name="monitorIndex">The monitor index to register the shader with</param>
/// <returns>True if registration succeeded</returns>
/// <remarks>
/// Register a custom shader that implements the ICustomShader interface.
/// Custom shaders allow you to apply custom rendering effects to the overlay.
///
/// Example:
/// <code>
/// // Create and register a custom shader
/// var shader = new MyCustomShader();
/// bool success = overlay.RegisterCustomShader(shader);
/// </code>
/// </remarks>
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<string, ICustomShader>();
}
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;
}
});
}
/// <summary>
/// Unregisters a custom shader
/// </summary>
/// <param name="shaderId">The ID of the shader to unregister</param>
/// <param name="monitorIndex">The monitor index the shader is registered with</param>
/// <returns>True if unregistration succeeded</returns>
/// <remarks>
/// Removes a previously registered custom shader and disposes its resources.
///
/// Example:
/// <code>
/// // Unregister a shader with ID "MyShader"
/// overlay.UnregisterCustomShader("MyShader");
/// </code>
/// </remarks>
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;
}
});
}
/// <summary>
/// Registers a custom renderable object for rendering with this overlay
/// </summary>
/// <param name="renderable">The custom renderable to register</param>
/// <returns>True if registration succeeded</returns>
/// <remarks>
/// 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:
/// <code>
/// // 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);
/// </code>
/// </remarks>
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<int, ICustomRenderable>();
}
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;
}
});
}
/// <summary>
/// Unregisters a custom renderable
/// </summary>
/// <param name="renderableId">The ID of the renderable to unregister</param>
/// <param name="monitorIndex">The monitor index the renderable is registered with</param>
/// <returns>True if unregistration succeeded</returns>
/// <remarks>
/// Removes a previously registered custom renderable object and disposes its resources.
///
/// Example:
/// <code>
/// // Unregister a renderable with ID 1
/// overlay.UnregisterCustomRenderable(1);
/// </code>
/// </remarks>
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;
}
});
}
/// <summary>
/// Updates a custom renderable to trigger a re-render
/// </summary>
/// <param name="renderableId">The ID of the renderable to update</param>
/// <param name="monitorIndex">The monitor index the renderable is on</param>
/// <remarks>
/// 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:
/// <code>
/// // 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);
/// </code>
/// </remarks>
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}");
}
});
}
/// <summary>
/// Gets a custom shader by ID
/// </summary>
/// <param name="shaderId">The ID of the shader to retrieve</param>
/// <param name="monitorIndex">The monitor index the shader is registered with</param>
/// <returns>The custom shader, or null if not found</returns>
/// <remarks>
/// Retrieves a previously registered custom shader by its ID.
///
/// Example:
/// <code>
/// // Get a registered shader
/// ICustomShader shader = overlay.GetCustomShader("MyShader");
/// if (shader != null)
/// {
/// // Use shader...
/// }
/// </code>
/// </remarks>
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;
}
}
/// <summary>
/// Queues a drawing action that returns a result
/// </summary>
private T QueueDrawingActionWithResult<T>(Func<T> 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;
}
/// <summary>
/// Gets the Direct3D device for a specific monitor
/// </summary>
/// <param name="monitorIndex">The monitor index</param>
/// <returns>The Direct3D device, or null if not available</returns>
/// <remarks>
/// 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:
/// <code>
/// // Get the device for the primary monitor
/// Device device = overlay.GetDeviceForMonitor(0);
/// if (device != null)
/// {
/// // Use device for advanced DirectX operations
/// }
/// </code>
/// </remarks>
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;
}
});
}
}
}