Skip to content

Commit b9a6d36

Browse files
fix: RGB Direct mode switching + overlay transparency crash + per-frame cache
RGB — the root cause of colours not reaching hardware: - Auto-switch all OpenRGB devices to Direct/Custom mode on connect - Without this, devices stay in Rainbow/Spectrum effects and ignore UpdateLeds() - Cache device layout in SetDeviceColorAsync/SetAllDevicesColorAsync - Eliminates per-frame GetAllControllerData() SDK round-trip (was 30x/sec) - Surface ModeSwitchStatus in IRgbService for UI feedback Overlay — AcrylicBlur is broken on Windows 11: - Replace AcrylicBlur with plain Transparent + alpha background - Add TransparencyBackgroundFallback to prevent ghost rendering - Fix double-alpha stacking (AXAML was semi-transparent + code-behind alpha) - Opaque mode now fully solid, no blur artifacts
1 parent 16046bb commit b9a6d36

5 files changed

Lines changed: 117 additions & 20 deletions

File tree

src/Fanzi.FanControl/Services/DirectRgbController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public sealed class DirectRgbController : IRgbService, IDisposable
2222

2323
public bool IsConnected => _virtualDevices.Count > 0 || (_bridge?.IsConnected ?? false);
2424
public string ServerVersion => _bridge?.ServerVersion ?? "IO-nity Direct RGB Engine v2.0";
25+
public string ModeSwitchStatus => _bridge?.ModeSwitchStatus ?? "Virtual devices (no mode switch needed)";
2526

2627
public IReadOnlyList<VirtualRgbDevice> VirtualDevices => _virtualDevices;
2728

src/Fanzi.FanControl/Services/IRgbService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ public interface IRgbService : IDisposable
1818
/// <summary>Server version string returned after handshake.</summary>
1919
string ServerVersion { get; }
2020

21+
/// <summary>
22+
/// Summary of the last mode-switch pass (e.g. "3 switched to Direct, 1 already Direct").
23+
/// Empty string if no mode switching has occurred yet.
24+
/// </summary>
25+
string ModeSwitchStatus { get; }
26+
2127
/// <summary>
2228
/// Attempts to connect to the OpenRGB server.
2329
/// Returns true on success; false if server is not reachable.

src/Fanzi.FanControl/Services/OpenRgbService.cs

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,26 @@ namespace Fanzi.FanControl.Services;
1010

1111
/// <summary>
1212
/// OpenRGB-backed implementation of <see cref="IRgbService"/>.
13-
/// Requires the OpenRGB application to be running with its SDK server enabled
14-
/// (Settings → SDK Server, default port 6742).
13+
/// Connects to the OpenRGB SDK server (default port 6742), auto-switches all
14+
/// discovered devices to Direct/Custom mode so per-LED colours actually reach hardware,
15+
/// and caches the device layout to avoid per-frame SDK round-trips.
1516
/// </summary>
1617
public sealed class OpenRgbService : IRgbService
1718
{
1819
private OpenRgbClient? _client;
19-
private OpenRGB.NET.Device[]? _deviceCache; // zone/LED layout, refreshed on each device scan
20+
private OpenRGB.NET.Device[]? _deviceCache;
2021
private bool _disposed;
2122
private readonly object _lock = new();
2223

2324
public bool IsConnected { get; private set; }
2425
public string ServerVersion { get; private set; } = "Not connected";
2526

27+
/// <summary>
28+
/// Summary of the last mode-switch pass — surfaced in the UI so users can see
29+
/// which devices were switched to Direct mode and which (if any) failed.
30+
/// </summary>
31+
public string ModeSwitchStatus { get; private set; } = "";
32+
2633
// ── Connection ────────────────────────────────────────────────────────────
2734

2835
public Task<bool> TryConnectAsync(
@@ -42,6 +49,7 @@ public Task<bool> TryConnectAsync(
4249
_deviceCache = null;
4350
IsConnected = false;
4451
ServerVersion = "Not connected";
52+
ModeSwitchStatus = "";
4553

4654
var client = new OpenRgbClient(
4755
ip: host,
@@ -75,6 +83,7 @@ public void Disconnect()
7583
_deviceCache = null;
7684
IsConnected = false;
7785
ServerVersion = "Disconnected";
86+
ModeSwitchStatus = "";
7887
}
7988
}
8089

@@ -93,7 +102,13 @@ public Task<IReadOnlyList<RgbDeviceInfo>> GetDevicesAsync(
93102
try
94103
{
95104
var devices = _client.GetAllControllerData();
96-
_deviceCache = devices; // cache layout so per-zone sends don't round-trip each frame
105+
_deviceCache = devices;
106+
107+
// Auto-switch every device to Direct/Custom mode so per-LED
108+
// colour updates actually reach the hardware. Without this,
109+
// devices stay in their built-in effect (Rainbow, Spectrum,
110+
// etc.) and silently ignore UpdateLeds() calls.
111+
SwitchAllDevicesToDirectMode(devices);
97112

98113
return devices
99114
.Select((d, i) =>
@@ -119,15 +134,87 @@ public Task<IReadOnlyList<RgbDeviceInfo>> GetDevicesAsync(
119134
})
120135
.ToArray();
121136
}
122-
catch
137+
catch (Exception ex)
123138
{
139+
ServerVersion = $"Error: {ex.Message.Split('\n')[0]}";
124140
IsConnected = false;
125141
return Array.Empty<RgbDeviceInfo>();
126142
}
127143
}
128144
}, cancellationToken);
129145
}
130146

147+
/// <summary>
148+
/// Iterates every discovered device and switches it to "Direct" or "Custom"
149+
/// mode — the mode that lets FANZI drive each LED individually.
150+
/// Devices already in Direct mode are left alone.
151+
/// </summary>
152+
private void SwitchAllDevicesToDirectMode(OpenRGB.NET.Device[] devices)
153+
{
154+
int switched = 0, already = 0, failed = 0;
155+
156+
for (int i = 0; i < devices.Length; i++)
157+
{
158+
try
159+
{
160+
var dev = devices[i];
161+
var activeMode = dev.ActiveMode;
162+
163+
// Already in Direct/Custom/Static-per-LED? Nothing to do.
164+
string modeName = activeMode.Name ?? "";
165+
bool isDirect = modeName.Contains("Direct", StringComparison.OrdinalIgnoreCase)
166+
|| modeName.Contains("Custom", StringComparison.OrdinalIgnoreCase)
167+
|| modeName.Contains("Static", StringComparison.OrdinalIgnoreCase);
168+
169+
if (isDirect)
170+
{
171+
already++;
172+
continue;
173+
}
174+
175+
// Find the Direct or Custom mode index on this device.
176+
int directIndex = -1;
177+
for (int m = 0; m < dev.Modes.Length; m++)
178+
{
179+
string name = dev.Modes[m].Name ?? "";
180+
if (name.Equals("Direct", StringComparison.OrdinalIgnoreCase)
181+
|| name.Equals("Custom", StringComparison.OrdinalIgnoreCase))
182+
{
183+
directIndex = m;
184+
break;
185+
}
186+
}
187+
188+
// Prefer "Direct" — if not found, try "Custom" via SetCustomMode.
189+
if (directIndex >= 0)
190+
{
191+
_client!.UpdateMode(i, directIndex);
192+
switched++;
193+
}
194+
else
195+
{
196+
// SetCustomMode sends the RGBController::SetCustomMode() command
197+
// which on most devices activates the per-LED direct control path.
198+
_client!.SetCustomMode(i);
199+
switched++;
200+
}
201+
}
202+
catch
203+
{
204+
failed++;
205+
}
206+
}
207+
208+
var parts = new List<string>();
209+
if (switched > 0) parts.Add($"{switched} switched to Direct");
210+
if (already > 0) parts.Add($"{already} already Direct");
211+
if (failed > 0) parts.Add($"{failed} failed");
212+
213+
ModeSwitchStatus = parts.Count > 0
214+
? string.Join(", ", parts)
215+
: "No devices";
216+
}
217+
131218
// ── Colour setting ────────────────────────────────────────────────────────
132219

133220
public Task SetDeviceColorAsync(
@@ -143,7 +230,8 @@ public Task SetDeviceColorAsync(
143230
if (_client is null || !IsConnected) return;
144231
try
145232
{
146-
var devices = _client.GetAllControllerData();
233+
// Use cached device layout — avoids a full SDK round-trip every frame.
234+
var devices = _deviceCache ?? _client.GetAllControllerData();
147235
if (deviceIndex < 0 || deviceIndex >= devices.Length) return;
148236

149237
int count = devices[deviceIndex].Leds.Length;
@@ -188,11 +276,13 @@ public Task SetAllDevicesColorAsync(
188276
if (_client is null || !IsConnected) return;
189277
try
190278
{
191-
var devices = _client.GetAllControllerData();
279+
// Use cached device layout — critical for performance.
280+
var devices = _deviceCache ?? _client.GetAllControllerData();
281+
var openColor = ToOpenRgb(color);
192282
for (int i = 0; i < devices.Length; i++)
193283
{
194284
var colors = Enumerable
195-
.Repeat(ToOpenRgb(color), devices[i].Leds.Length)
285+
.Repeat(openColor, devices[i].Leds.Length)
196286
.ToArray();
197287
_client.UpdateLeds(i, colors);
198288
}
@@ -215,16 +305,12 @@ public Task SetDeviceZoneColorsAsync(
215305
if (_client is null || !IsConnected) return;
216306
try
217307
{
218-
// Use the cached layout where possible so we don't hit the SDK every frame.
219308
var devices = _deviceCache ?? _client.GetAllControllerData();
220309
if (deviceIndex < 0 || deviceIndex >= devices.Length) return;
221310

222311
var dev = devices[deviceIndex];
223312
int ledTotal = dev.Leds.Length;
224313

225-
// Build the full per-LED buffer for the device, expanding each zone's
226-
// colour across its LED span. Pre-fill black so unzoned/trailing LEDs
227-
// are always initialised (safe whether Color is a struct or class).
228314
var black = ToOpenRgb(RgbColor.Black);
229315
var leds = new OpenRGB.NET.Color[ledTotal];
230316
for (int k = 0; k < ledTotal; k++) leds[k] = black;

src/Fanzi.FanControl/Views/MiniOverlayWindow.axaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
ShowInTaskbar="False"
1010
CanResize="False"
1111
SystemDecorations="None"
12-
Background="#CC050A12"
12+
Background="#080E18"
13+
TransparencyLevelHint="Transparent"
14+
TransparencyBackgroundFallback="#050A12"
1315
WindowStartupLocation="Manual">
1416

1517
<Window.Styles>

src/Fanzi.FanControl/Views/MiniOverlayWindow.axaml.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ public MiniOverlayWindow()
2929
var closeBtn = this.FindControl<Button>("CloseBtn");
3030
if (closeBtn is not null) closeBtn.Click += (_, _) => Hide();
3131

32-
// Apply transparency from settings when DataContext is set
3332
DataContextChanged += OnDataContextChanged;
3433
}
3534

@@ -40,7 +39,6 @@ private void OnDataContextChanged(object? sender, EventArgs e)
4039
_vm = vm;
4140
ApplyTransparency();
4241

43-
// Listen for changes
4442
vm.PropertyChanged += (s, args) =>
4543
{
4644
if (args.PropertyName == nameof(MainWindowViewModel.OverlayTransparent) ||
@@ -56,17 +54,21 @@ private void ApplyTransparency()
5654
{
5755
if (_vm is null) return;
5856

57+
byte alpha = (byte)(Math.Clamp(_vm.OverlayOpacity, 0.1, 1.0) * 255);
58+
5959
if (_vm.OverlayTransparent)
6060
{
61-
// Transparent mode: acrylic blur + semi-transparent background
62-
TransparencyLevelHint = new[] { Avalonia.Controls.WindowTransparencyLevel.AcrylicBlur };
63-
byte alpha = (byte)(Math.Clamp(_vm.OverlayOpacity, 0.1, 1.0) * 255);
61+
// Use solid colour with alpha — avoids AcrylicBlur which is broken
62+
// on many Windows 11 builds (flickering, garbage pixels, ghost windows).
63+
TransparencyLevelHint = new[] { WindowTransparencyLevel.Transparent };
64+
TransparencyBackgroundFallback = new SolidColorBrush(Color.FromArgb(255, 5, 10, 18));
6465
Background = new SolidColorBrush(Color.FromArgb(alpha, 5, 10, 18));
6566
}
6667
else
6768
{
68-
// Opaque mode: solid dark background, no blur
69-
TransparencyLevelHint = new[] { Avalonia.Controls.WindowTransparencyLevel.None };
69+
// Fully opaque — no transparency at all.
70+
TransparencyLevelHint = new[] { WindowTransparencyLevel.None };
71+
TransparencyBackgroundFallback = new SolidColorBrush(Color.FromArgb(255, 8, 14, 24));
7072
Background = new SolidColorBrush(Color.FromArgb(255, 8, 14, 24));
7173
}
7274
}

0 commit comments

Comments
 (0)