diff --git a/KeyLighting/CPUImageProcessor.cs b/KeyLighting/CPUImageProcessor.cs index 5e18f26..2a1cc11 100644 --- a/KeyLighting/CPUImageProcessor.cs +++ b/KeyLighting/CPUImageProcessor.cs @@ -1,41 +1,62 @@ using System; -using System.Collections.Concurrent; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading.Tasks; +using KeyboardLighting; +using Newtonsoft.Json; using OpenRGB.NET; + public class CPUImageProcessor : IDisposable { - private byte[]? pixelBuffer; - private int lastWidth; - private int lastHeight; - private readonly object bufferLock = new object(); - private byte[] brightnessLut; - private byte[] contrastLut; - private bool lutsInitialized = false; + private byte[] brightnessLut = new byte[256]; + private byte[] contrastLut = new byte[256]; private double lastBrightness = -1; private double lastContrast = -1; - public OpenRGB.NET.Color[] ProcessImage(Bitmap image, int targetWidth, int targetHeight, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor) - { + private OpenRGB.NET.Color[] previousFrame; + private OpenRGB.NET.Color[] rawColors; + private OpenRGB.NET.Color[] resultBuffer; + private bool hasPreviousFrame = false; + + private double fadeSpeed; - return ProcessImageOnCPU(image, targetWidth, targetHeight, brightness, vibrance, contrast, darkThreshold, darkFactor); + public CPUImageProcessor(LightingConfig config) + { + fadeSpeed = config.FadeFactor; } - private OpenRGB.NET.Color[] resultBuffer; + private bool lastFrameWasSolid = false; + private int lastSolidR, lastSolidG, lastSolidB; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetFadeSpeed(double speed) + { + fadeSpeed = Math.Clamp(speed, 0.0, 1.0); + } - private OpenRGB.NET.Color[] ProcessImageOnCPU(Bitmap image, int targetWidth, int targetHeight, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor) + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public OpenRGB.NET.Color[] ProcessImage(Bitmap image, int targetWidth, int targetHeight, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor) { if (resultBuffer == null || resultBuffer.Length != targetWidth) { resultBuffer = new OpenRGB.NET.Color[targetWidth]; + rawColors = new OpenRGB.NET.Color[targetWidth]; + previousFrame = new OpenRGB.NET.Color[targetWidth]; + hasPreviousFrame = false; + } + + if (!AreSettingsCached(brightness, contrast)) + { + InitializeLuts(brightness, contrast); + lastBrightness = brightness; + lastContrast = contrast; } Bitmap bitmapToProcess = image; @@ -74,15 +95,29 @@ private OpenRGB.NET.Color[] ProcessImageOnCPU(Bitmap image, int targetWidth, int Marshal.Copy(bmpData.Scan0, pixelBuffer, 0, byteCount); - if (bytesPerPixel == 4) + ExtractColumns(bmpData.Stride, targetWidth, targetHeight, bytesPerPixel); + + bool isSolidColor = IsSolidColorFrame(); + + if (isSolidColor) { - ProcessColumns32Bpp(bmpData.Stride, targetWidth, targetHeight, brightness, vibrance, contrast, darkThreshold, darkFactor); + ProcessSolidColor(rawColors[0].R, rawColors[0].G, rawColors[0].B, + targetWidth, brightness, vibrance, contrast, + darkThreshold, darkFactor); } - else if (bytesPerPixel == 3) + else { - ProcessColumns24Bpp(bmpData.Stride, targetWidth, targetHeight, brightness, vibrance, contrast, darkThreshold, darkFactor); + + ProcessColumnsWithEffects(targetWidth, brightness, vibrance, contrast, darkThreshold, darkFactor); + + if (hasPreviousFrame) + { + ApplyFading(targetWidth, lastFrameWasSolid ? 0.95 : fadeSpeed); + } } + StoreFrameState(targetWidth, isSolidColor); + return resultBuffer; } finally @@ -100,7 +135,112 @@ private OpenRGB.NET.Color[] ProcessImageOnCPU(Bitmap image, int targetWidth, int } [MethodImpl(MethodImplOptions.AggressiveOptimization)] - private void ProcessColumns24Bpp(int stride, int width, int height, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor) + private void ProcessSolidColor(byte r, byte g, byte b, int width, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor) + { + + OpenRGB.NET.Color processedColor = FastApplyEffects(r, g, b, brightness, vibrance, contrast, darkThreshold, darkFactor); + + bool needsFade = hasPreviousFrame && !(lastFrameWasSolid && + lastSolidR == processedColor.R && + lastSolidG == processedColor.G && + lastSolidB == processedColor.B); + + if (needsFade) + { + + double fadeFactor = 0.95; + + Parallel.For(0, width, i => { + resultBuffer[i] = FastBlendColors(previousFrame[i], processedColor, fadeFactor); + }); + } + else + { + + for (int i = 0; i < width; i++) + { + resultBuffer[i] = processedColor; + } + } + + lastSolidR = processedColor.R; + lastSolidG = processedColor.G; + lastSolidB = processedColor.B; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool AreSettingsCached(double brightness, double contrast) + { + return Math.Abs(lastBrightness - brightness) <= 0.001 && Math.Abs(lastContrast - contrast) <= 0.001; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void StoreFrameState(int width, bool isSolidColor) + { + + Array.Copy(resultBuffer, previousFrame, width); + hasPreviousFrame = true; + lastFrameWasSolid = isSolidColor; + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + private bool IsSolidColorFrame() + { + if (rawColors.Length < 2) return true; + + OpenRGB.NET.Color first = rawColors[0]; + const int tolerance = 5; + + int[] samplePoints = { 0, rawColors.Length / 3, rawColors.Length / 2, (rawColors.Length * 2) / 3, rawColors.Length - 1 }; + + foreach (int i in samplePoints) + { + if (i == 0) continue; + + if (Math.Abs(first.R - rawColors[i].R) > tolerance || + Math.Abs(first.G - rawColors[i].G) > tolerance || + Math.Abs(first.B - rawColors[i].B) > tolerance) + { + return false; + } + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + private void ApplyFading(int width, double fadeFactor) + { + Parallel.For(0, width, i => { + resultBuffer[i] = FastBlendColors(previousFrame[i], resultBuffer[i], fadeFactor); + }); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OpenRGB.NET.Color FastBlendColors(OpenRGB.NET.Color color1, OpenRGB.NET.Color color2, double factor) + { + + factor = Math.Clamp(factor, 0.0, 1.0); + double inverseFactor = 1.0 - factor; + + byte r = (byte)(color1.R * inverseFactor + color2.R * factor); + byte g = (byte)(color1.G * inverseFactor + color2.G * factor); + byte b = (byte)(color1.B * inverseFactor + color2.B * factor); + + return new OpenRGB.NET.Color(r, g, b); + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + private void ExtractColumns(int stride, int width, int height, int bytesPerPixel) + { + if (bytesPerPixel == 4) + ExtractColumns32Bpp(stride, width, height); + else + ExtractColumns24Bpp(stride, width, height); + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + private void ExtractColumns24Bpp(int stride, int width, int height) { Parallel.For(0, width, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, x => { @@ -110,7 +250,20 @@ private void ProcessColumns24Bpp(int stride, int width, int height, double brigh int pixelCount = height; int columnOffset = x * 3; - for (int y = 0; y < height; y++) + int y = 0; + for (; y < height - 3; y += 4) + { + int offset1 = y * stride + columnOffset; + int offset2 = (y + 1) * stride + columnOffset; + int offset3 = (y + 2) * stride + columnOffset; + int offset4 = (y + 3) * stride + columnOffset; + + totalB += (uint)pixelBuffer[offset1] + pixelBuffer[offset2] + pixelBuffer[offset3] + pixelBuffer[offset4]; + totalG += (uint)pixelBuffer[offset1 + 1] + pixelBuffer[offset2 + 1] + pixelBuffer[offset3 + 1] + pixelBuffer[offset4 + 1]; + totalR += (uint)pixelBuffer[offset1 + 2] + pixelBuffer[offset2 + 2] + pixelBuffer[offset3 + 2] + pixelBuffer[offset4 + 2]; + } + + for (; y < height; y++) { int offset = y * stride + columnOffset; totalB += pixelBuffer[offset]; @@ -122,13 +275,13 @@ private void ProcessColumns24Bpp(int stride, int width, int height, double brigh byte avgG = (byte)(totalG / pixelCount); byte avgB = (byte)(totalB / pixelCount); - resultBuffer[x] = FastApplyEffects(avgR, avgG, avgB, brightness, vibrance, contrast, darkThreshold, darkFactor); + rawColors[x] = new OpenRGB.NET.Color(avgR, avgG, avgB); } }); } [MethodImpl(MethodImplOptions.AggressiveOptimization)] - private void ProcessColumns32Bpp(int stride, int width, int height, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor) + private void ExtractColumns32Bpp(int stride, int width, int height) { Parallel.For(0, width, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, x => { @@ -138,7 +291,20 @@ private void ProcessColumns32Bpp(int stride, int width, int height, double brigh int pixelCount = height; int columnOffset = x * 4; - for (int y = 0; y < height; y++) + int y = 0; + for (; y < height - 3; y += 4) + { + int offset1 = y * stride + columnOffset; + int offset2 = (y + 1) * stride + columnOffset; + int offset3 = (y + 2) * stride + columnOffset; + int offset4 = (y + 3) * stride + columnOffset; + + totalB += (uint)(pixelBuffer[offset1] + pixelBuffer[offset2] + pixelBuffer[offset3] + pixelBuffer[offset4]); + totalG += (uint)pixelBuffer[offset1 + 1] + pixelBuffer[offset2 + 1] + pixelBuffer[offset3 + 1] + pixelBuffer[offset4 + 1]; + totalR += (uint)pixelBuffer[offset1 + 2] + pixelBuffer[offset2 + 2] + pixelBuffer[offset3 + 2] + pixelBuffer[offset4 + 2]; + } + + for (; y < height; y++) { int offset = y * stride + columnOffset; totalB += pixelBuffer[offset]; @@ -150,23 +316,24 @@ private void ProcessColumns32Bpp(int stride, int width, int height, double brigh byte avgG = (byte)(totalG / pixelCount); byte avgB = (byte)(totalB / pixelCount); - resultBuffer[x] = FastApplyEffects(avgR, avgG, avgB, brightness, vibrance, contrast, darkThreshold, darkFactor); + rawColors[x] = new OpenRGB.NET.Color(avgR, avgG, avgB); } }); } [MethodImpl(MethodImplOptions.AggressiveOptimization)] - private OpenRGB.NET.Color FastApplyEffects(byte r, byte g, byte b, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor) + private void ProcessColumnsWithEffects(int width, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor) { - - if (!lutsInitialized || Math.Abs(lastBrightness - brightness) > 0.001 || Math.Abs(lastContrast - contrast) > 0.001) + Parallel.For(0, width, x => { - InitializeLuts(brightness, contrast); - lastBrightness = brightness; - lastContrast = contrast; - lutsInitialized = true; - } + resultBuffer[x] = FastApplyEffects(rawColors[x].R, rawColors[x].G, rawColors[x].B, + brightness, vibrance, contrast, darkThreshold, darkFactor); + }); + } + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + private OpenRGB.NET.Color FastApplyEffects(byte r, byte g, byte b, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor) + { unchecked { @@ -217,29 +384,28 @@ private OpenRGB.NET.Color FastApplyEffects(byte r, byte g, byte b, double bright bVal = bVal < darkThreshold ? bDark : bVal; } - return new OpenRGB.NET.Color((byte)rVal, (byte)gVal, (byte)bVal); + return new OpenRGB.NET.Color( + (byte)Math.Min(Math.Max(rVal, 0), 255), + (byte)Math.Min(Math.Max(gVal, 0), 255), + (byte)Math.Min(Math.Max(bVal, 0), 255) + ); } } + [MethodImpl(MethodImplOptions.AggressiveOptimization)] private void InitializeLuts(double brightness, double contrast) { - if (brightnessLut == null) - { - brightnessLut = new byte[256]; - contrastLut = new byte[256]; - } - for (int i = 0; i < 256; i++) { int brightVal = (int)(i * brightness); - brightnessLut[i] = (byte)Math.Min(brightVal, 255); + brightnessLut[i] = (byte)Math.Min(Math.Max(brightVal, 0), 255); if (Math.Abs(contrast - 1.0) > 0.001) { double normalized = i / 255.0; - int contrastVal = (int)(Math.Pow(normalized, contrast) * 255.0); - contrastLut[i] = (byte)Math.Min(Math.Max(contrastVal, 0), 255); + double adjusted = Math.Pow(normalized, contrast) * 255.0; + contrastLut[i] = (byte)Math.Min(Math.Max((int)adjusted, 0), 255); } else { @@ -252,9 +418,9 @@ public void Dispose() { pixelBuffer = null; resultBuffer = null; + rawColors = null; + previousFrame = null; brightnessLut = null; contrastLut = null; - GC.Collect(); - GC.WaitForPendingFinalizers(); } } \ No newline at end of file diff --git a/KeyLighting/Program.cs b/KeyLighting/Program.cs index 0a54734..2fced74 100644 --- a/KeyLighting/Program.cs +++ b/KeyLighting/Program.cs @@ -77,7 +77,8 @@ static void Main(string[] args) fadeProgress = new float[ledCount]; targetColors = new ORGBColor[ledCount]; - using var processor = new CPUImageProcessor(); + var processor = new CPUImageProcessor(config); + using var capturer = ConfigureScreenCapturer(config); var updateTimer = new System.Timers.Timer(config.UpdateDelayMs); diff --git a/KeyLighting/config.json b/KeyLighting/config.json index b1fd457..c96c77d 100644 --- a/KeyLighting/config.json +++ b/KeyLighting/config.json @@ -6,7 +6,7 @@ "brightnessMultiplier": 1.8, "vibranceFactor": 1, "contrastPower": 1.8, - "fadeFactor": 3, + "fadeFactor": 0.9, "darkenThreshold": 65, "darkenFactor": 0.4, "wasdEnabled": false, diff --git a/KeyLighting/models/config.cs b/KeyLighting/models/config.cs index 8474153..2e51cfa 100644 --- a/KeyLighting/models/config.cs +++ b/KeyLighting/models/config.cs @@ -101,7 +101,11 @@ public class CaptureRegionConfig public int Height { get; set; } = 1080; } } - + public static class AppSettings + { + public static LightingConfig Config { get; set; } + } + // Load configuration from file public static LightingConfig LoadFromFile(string filePath)