Add priority mode + always-on network packet priority

Priority mode (per-profile toggle, Audio profile tab, Alt+U):
- Bundled PROCESS_POWER_THROTTLING_STATE with EXECUTION_SPEED and
  IGNORE_TIMER_RESOLUTION flags (keeps CPU at full clock + 1 ms quantum
  pinned regardless of OS idle decisions)
- PowerSetRequest(EXECUTION_REQUIRED) + SetThreadExecutionState so the
  system can't decide we're idle and downclock
- timeBeginPeriod(1) for 1 ms scheduler quantum
- ProcessPriorityClass.High, ProcessMemoryPriority normal, working-set
  minimum locked via SetProcessWorkingSetSizeEx(HARDWS_MIN_ENABLE)
- Fully reversed on toggle-off and on form close
- Off by default (battery cost on laptops); explicit opt-in per profile

MMCSS thread priority on hot threads bumped from High to Critical
(network listener, mix loop, render thread) — always on.

Network packet priority (always on, no toggle):
- qWAVE flow on the outbound UDP socket at QOS_TRAFFIC_TYPE_VOICE with
  QOS_NON_ADAPTIVE_FLOW (Voice priority, DSCP 46/EF, WMM Voice on Wi-Fi)
- Silent fallback if qwave.dll missing or QoS service disabled
- Sender + receiver kernel UDP buffers bumped to 1 MB each (was
  256 KB send / 512 KB receive) for resilience against ~30 ms stalls

Bug fix: PreferencesDialog logging toggle no longer flags the profile
as dirty. Logging is a machine-local AppConfig setting; the spurious
ChangedAnyProfileSetting flag was triggering the unsaved-changes prompt
on exit after toggling logs alone.

Manual updated: new "Use CPU and Windows performance settings in high
priority mode (Alt+U)" subsection on the Audio profile tab, new
"Network packet priority (qWAVE / Voice / WMM)" subsection in the
network chapter, Alt+U row added to keyboard-shortcuts table.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-05-14 11:21:58 +01:00
co-authored by Claude Opus 4.7
parent 146e76c6e6
commit 768875cebb
10 changed files with 731 additions and 14 deletions
+47 -5
View File
@@ -187,6 +187,19 @@ public sealed class MainForm : Form
private readonly ListBox smoothnessBox = new() { Width = 420, Height = 200, IntegralHeight = false, AccessibleName = "Buffer smoothness (Alt+B)" };
private readonly ListBox artefactBox = new() { Width = 420, Height = 60, IntegralHeight = false, AccessibleName = "Artefact sound type (Alt+A) — controls how audio gaps sound" };
private readonly AccessibleCheckBox tightLatencyBox = new() { AutoSize = true };
// Priority mode (per profile). Sits as the first control on the Audio profile tab,
// ungrouped above the two GroupBoxes, so it's the first thing focus lands on when the
// user Tabs into the tab. Toggling marks the profile dirty (the setting lives in
// Profile, not AppConfig) and flips every PerformanceMode lever in one shot — CPU
// scheduling, Windows power management, memory priority, working-set lock, and
// MMCSS thread priority. Label deliberately mentions both "CPU" and "Windows
// performance settings" because the toggle reaches well past just CPU scheduling.
private readonly AccessibleCheckBox priorityModeBox = new()
{
Text = "&Use CPU and Windows performance settings in high priority mode (Alt+U)",
AccessibleName = "Use CPU and Windows performance settings in high priority mode",
AutoSize = true,
};
// --- Connectivity tab controls (Phase 2 refactor) ---
private readonly LiveCheckedListBox connectedPeersList = new() { CheckOnClick = true, Width = 430, Height = 90, AccessibleName = "Connected peers (Alt+C)" };
@@ -626,6 +639,12 @@ public sealed class MainForm : Form
// having to infer from sender-engine restarts. Includes the audio mode because what
// "tight" means is mode-dependent (per-callback ASIO emission vs. WASAPI push-mode).
logFile.Event($"tight latency at startup: {(initialTightLatency ? "on" : "off")} (audio mode={settings.LoadAudioMode()})");
// Priority mode (per-profile). Applies every PerformanceMode lever on first launch
// under this profile so the OS doesn't start coasting before the user has tabbed
// onto the Audio profile tab. The Audio-profile tab's checkbox handler re-applies
// on every toggle.
PerformanceMode.Apply(settings.LoadPriorityMode(), msg => logFile.Event(msg));
// Native-rate passthrough is automatic now (driven by codec, not a user setting):
// PCM+single-source-WASAPI-push = pass capture-device rate through to the wire;
// Opus = always pre-resample to 48 kHz (encoder is locked at 48 k); MixingEngine /
@@ -801,6 +820,10 @@ public sealed class MainForm : Form
continuousTuneTimer.Stop();
updateCheckTimer.Stop();
asioDriverChangeDebounce.Stop();
// Reverse every Win32 lever PerformanceMode applied. The kernel would clean
// these up on process exit anyway, but doing it explicitly releases the power
// request handle and matches our timeBeginPeriod with a timeEndPeriod.
try { PerformanceMode.Apply(false, msg => logFile.Event(msg)); } catch { /* harmless */ }
try { discovery.Dispose(); } catch { }
try { heartbeatService?.Dispose(); } catch { }
@@ -1522,19 +1545,37 @@ public sealed class MainForm : Form
/// mirrors of hidden form-fields.</summary>
private void BuildAudioProfileTab()
{
// Outer layout: one column, two rows — one row per GroupBox. AutoScroll on so the
// tab page handles overflow rather than the inner groups clipping their contents.
// Outer layout: one column, three rows. Row 0 is the Full-CPU-speed checkbox — the
// first thing the user lands on when they Tab into the tab, deliberately ungrouped
// and at the top so it can't be missed. Rows 1 and 2 are the existing Audio send
// parameters / Audio receive parameters GroupBoxes. AutoScroll on so the tab page
// handles overflow rather than the inner groups clipping their contents.
var outerPanel = new TableLayoutPanel
{
Dock = DockStyle.Fill,
Padding = new Padding(12),
ColumnCount = 1,
RowCount = 2,
RowCount = 3,
AutoScroll = true,
};
outerPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
outerPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
outerPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
outerPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
// Wrap the checkbox in its own FlowLayoutPanel — same NVDA-friendly pattern the
// form's other top-level checkboxes use (a bare CheckBox in a TableLayoutPanel
// cell suppresses some state-change announcements; the FlowLayoutPanel restores
// the announcement chain).
var priorityModePanel = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill };
priorityModePanel.Controls.Add(priorityModeBox);
priorityModeBox.Checked = settings.LoadPriorityMode();
priorityModeBox.CheckedChanged += (_, _) =>
{
settings.SavePriorityMode(priorityModeBox.Checked);
PerformanceMode.Apply(priorityModeBox.Checked, msg => logFile.Event(msg));
MarkProfileDirty();
};
var sendGroup = new GroupBox
{
@@ -1554,8 +1595,9 @@ public sealed class MainForm : Form
BuildAudioSendGroupContents(sendGroup);
BuildAudioReceiveGroupContents(receiveGroup);
outerPanel.Controls.Add(sendGroup, 0, 0);
outerPanel.Controls.Add(receiveGroup, 0, 1);
outerPanel.Controls.Add(priorityModePanel, 0, 0);
outerPanel.Controls.Add(sendGroup, 0, 1);
outerPanel.Controls.Add(receiveGroup, 0, 2);
audioProfileTabPage.Controls.Add(outerPanel);
}
+425
View File
@@ -0,0 +1,425 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace RemSound.App;
/// <summary>
/// "Full CPU speed" mode for the current profile. When enabled, pulls every documented
/// Windows lever to keep our process running at full clock — no EcoQoS downclocking, no
/// migration to E-cores, no deep-C-state idling, 1 ms scheduler quantum, High priority
/// class. All five mechanisms are per-process or per-thread: nothing affects other apps,
/// and nothing here changes the system power plan (which is global) or anyone else's
/// scheduling. Untoggling reverses every change cleanly.
///
/// Mechanisms layered on:
/// <list type="number">
/// <item><b>Process power throttling — opt out of EXECUTION_SPEED</b>. This is the
/// main one. Tells Windows' scheduler "this is not a background process; don't
/// downclock it, don't migrate its threads onto efficiency cores on hybrid CPUs".
/// Without this, Windows aggressively saves power on processes it thinks are
/// idle, which is the root cause of the "cold start sounds awful, warms up after
/// a few seconds" behaviour the user reported. Same API Discord, Spotify, OBS
/// and similar real-time apps use.</item>
/// <item><b>PowerSetRequest(EXECUTION_REQUIRED)</b>. Tells the kernel the system as a
/// whole can't enter deep low-power C-states while this process is active. Belt
/// and braces with #1 — covers the case where #1 isn't honoured (very old build
/// of Windows 10, some custom OEM image, etc.).</item>
/// <item><b>Process priority class = High</b>. Standard for low-latency audio apps;
/// bumps our dispatch priority above almost everything except OS internals. We
/// deliberately do <i>not</i> use Realtime — that class can starve OS services
/// (including the audio service itself) and is widely documented as causing
/// worse audio behaviour, not better.</item>
/// <item><b>timeBeginPeriod(1)</b>. Asks Windows for a 1 ms scheduler quantum. Helps
/// any code path that does timer-based waiting. Since Windows 10 build 1803,
/// this is scoped per-process — so we don't damage other apps' scheduling. We
/// match every <c>timeBeginPeriod</c> with a <c>timeEndPeriod</c> when the
/// feature is disabled, otherwise the OS keeps the elevated rate forever (the
/// old global-period gotcha is long gone but we're tidy anyway).</item>
/// <item><b>SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)</b>. Older
/// API but additive to #2; prevents the system from idle-sleeping. Negligible
/// cost. ES_DISPLAY_REQUIRED is deliberately omitted — we don't need the
/// screen on while we're running, just the CPU.</item>
/// </list>
///
/// Trade-offs the user should know about:
/// <list type="bullet">
/// <item>Laptop on battery: faster drain while RemSound is open with this on. Worth
/// the energy for a "live session" profile; not worth it for a "background
/// listening" one. That's why this lives on the Profile, not on AppConfig.</item>
/// <item>Desktop on mains: usually a couple of watts more, no practical downside.</item>
/// </list>
///
/// Idempotent — calling <see cref="Apply"/> with the same value twice is a no-op. The
/// internal state tracks the active power request handle so <see cref="Apply(bool, Action{string})"/>
/// with <c>enable: false</c> releases every resource it acquired. Safe to call from the
/// UI thread; none of the underlying API calls block in any meaningful sense.
/// </summary>
internal static class PerformanceMode
{
private static readonly object gate = new();
private static bool currentlyEnabled;
private static IntPtr powerRequestHandle = IntPtr.Zero;
private static ProcessPriorityClass? priorityBeforeBoost;
/// <summary>Apply or reverse Full-CPU-speed mode. <paramref name="log"/> receives a
/// short status line so the activation is visible in the diagnostic log file.
/// Failures on individual mechanisms (e.g. an OEM image rejecting one of the Win32
/// calls) are logged but don't abort the rest of the application of the mode.</summary>
public static void Apply(bool enable, Action<string>? log = null)
{
lock (gate)
{
if (enable == currentlyEnabled)
{
log?.Invoke($"performance mode: already {(enable ? "on" : "off")}, no change");
return;
}
if (enable)
{
TrySetPowerThrottling(disable: true, log);
TryStartPowerRequest(log);
TryRaisePriority(log);
TryBeginTimePeriod(log);
TrySetThreadExecutionState(keepAwake: true, log);
TrySetMemoryPriorityNormal(log);
TryLockWorkingSetMin(log);
currentlyEnabled = true;
log?.Invoke("priority mode: ON (EcoQoS off, timer resolution honored, power-request set, priority High, 1 ms quantum, system-required, memory priority normal, working-set min locked)");
}
else
{
TrySetPowerThrottling(disable: false, log);
TryStopPowerRequest(log);
TryRestorePriority(log);
TryEndTimePeriod(log);
TrySetThreadExecutionState(keepAwake: false, log);
TryRelaxWorkingSet(log);
currentlyEnabled = false;
log?.Invoke("priority mode: OFF (all overrides cleared, defaults restored)");
}
}
}
// === 1. Process power throttling (EcoQoS opt-out + timer-resolution honoring) ===
private static void TrySetPowerThrottling(bool disable, Action<string>? log)
{
try
{
// We control two flags at once:
// * EXECUTION_SPEED — StateMask=0 means "don't throttle this process"
// (no downclock, no E-core migration). The main lever.
// * IGNORE_TIMER_RESOLUTION — StateMask=0 means "honour my
// timeBeginPeriod request even if other apps don't want fine grain".
// Pairs naturally with the timeBeginPeriod(1) call further down.
// When we revert (disable=false) we set ControlMask=0, which hands both
// flags back to Windows' system-default decision-making rather than
// forcing them on.
var state = new PROCESS_POWER_THROTTLING_STATE
{
Version = ProcessPowerThrottlingCurrentVersion,
ControlMask = disable
? (ProcessPowerThrottlingExecutionSpeed | ProcessPowerThrottlingIgnoreTimerResolution)
: 0u,
StateMask = 0u,
};
var ok = SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling,
ref state, Marshal.SizeOf<PROCESS_POWER_THROTTLING_STATE>());
if (!ok) log?.Invoke($"priority mode: SetProcessInformation(power throttling) failed (win32={Marshal.GetLastWin32Error()})");
}
catch (Exception ex)
{
log?.Invoke($"priority mode: power throttling threw: {ex.GetType().Name}: {ex.Message}");
}
}
// === 2. Kernel power request (no deep C-states) ===
private static void TryStartPowerRequest(Action<string>? log)
{
try
{
var ctx = new REASON_CONTEXT
{
Version = PowerRequestContextVersion,
Flags = PowerRequestContextSimpleString,
Reason = "RemSound is running with Full CPU speed enabled.",
};
var handle = PowerCreateRequest(ref ctx);
if (handle == new IntPtr(-1))
{
log?.Invoke($"performance mode: PowerCreateRequest failed (win32={Marshal.GetLastWin32Error()})");
return;
}
if (!PowerSetRequest(handle, PowerRequestExecutionRequired))
{
log?.Invoke($"performance mode: PowerSetRequest failed (win32={Marshal.GetLastWin32Error()})");
CloseHandle(handle);
return;
}
powerRequestHandle = handle;
}
catch (Exception ex)
{
log?.Invoke($"performance mode: power request threw: {ex.GetType().Name}: {ex.Message}");
}
}
private static void TryStopPowerRequest(Action<string>? log)
{
if (powerRequestHandle == IntPtr.Zero) return;
try
{
PowerClearRequest(powerRequestHandle, PowerRequestExecutionRequired);
CloseHandle(powerRequestHandle);
}
catch (Exception ex)
{
log?.Invoke($"performance mode: power-request release threw: {ex.GetType().Name}: {ex.Message}");
}
finally
{
powerRequestHandle = IntPtr.Zero;
}
}
// === 3. Process priority class ===
private static void TryRaisePriority(Action<string>? log)
{
try
{
using var proc = Process.GetCurrentProcess();
priorityBeforeBoost = proc.PriorityClass;
proc.PriorityClass = ProcessPriorityClass.High;
}
catch (Exception ex)
{
log?.Invoke($"performance mode: priority raise threw: {ex.GetType().Name}: {ex.Message}");
}
}
private static void TryRestorePriority(Action<string>? log)
{
if (priorityBeforeBoost is null) return;
try
{
using var proc = Process.GetCurrentProcess();
proc.PriorityClass = priorityBeforeBoost.Value;
}
catch (Exception ex)
{
log?.Invoke($"performance mode: priority restore threw: {ex.GetType().Name}: {ex.Message}");
}
finally
{
priorityBeforeBoost = null;
}
}
// === 4. timeBeginPeriod / timeEndPeriod ===
private static bool timePeriodActive;
private static void TryBeginTimePeriod(Action<string>? log)
{
if (timePeriodActive) return;
try
{
if (timeBeginPeriod(1) == 0)
{
timePeriodActive = true;
}
else
{
log?.Invoke("performance mode: timeBeginPeriod(1) failed");
}
}
catch (Exception ex)
{
log?.Invoke($"performance mode: timeBeginPeriod threw: {ex.GetType().Name}: {ex.Message}");
}
}
private static void TryEndTimePeriod(Action<string>? log)
{
if (!timePeriodActive) return;
try
{
timeEndPeriod(1);
}
catch (Exception ex)
{
log?.Invoke($"performance mode: timeEndPeriod threw: {ex.GetType().Name}: {ex.Message}");
}
finally
{
timePeriodActive = false;
}
}
// === 5. SetThreadExecutionState ===
private static void TrySetThreadExecutionState(bool keepAwake, Action<string>? log)
{
try
{
var flags = keepAwake
? ES_CONTINUOUS | ES_SYSTEM_REQUIRED
: ES_CONTINUOUS;
var prev = SetThreadExecutionState(flags);
if (prev == 0) log?.Invoke("priority mode: SetThreadExecutionState returned 0 (call rejected)");
}
catch (Exception ex)
{
log?.Invoke($"priority mode: SetThreadExecutionState threw: {ex.GetType().Name}: {ex.Message}");
}
}
// === 6. Process memory priority ===
// Setting our process's memory priority explicitly to NORMAL. The OS default is
// already NORMAL (5), but Windows can demote "background-looking" processes
// (especially after long idle periods or under memory pressure), and a demoted
// process gets its working-set pages evicted first when the trimmer runs. This
// call is a defensive assertion: "I am a foreground-class process, treat my
// pages accordingly".
private static void TrySetMemoryPriorityNormal(Action<string>? log)
{
try
{
var info = new MEMORY_PRIORITY_INFORMATION { MemoryPriority = MemoryPriorityNormal };
var ok = SetProcessInformationMemory(GetCurrentProcess(), ProcessMemoryPriority,
ref info, Marshal.SizeOf<MEMORY_PRIORITY_INFORMATION>());
if (!ok) log?.Invoke($"priority mode: SetProcessInformation(memory) failed (win32={Marshal.GetLastWin32Error()})");
}
catch (Exception ex)
{
log?.Invoke($"priority mode: memory priority threw: {ex.GetType().Name}: {ex.Message}");
}
}
// === 7. Lock the minimum working-set size ===
// Windows trims a process's working set proactively when the machine looks idle.
// For RemSound that's exactly the moment we'd rather it didn't — the first packet
// after an idle stretch hits a cold cache and pages have to be brought back in,
// which audibly stalls the first frames. Locking a minimum working set (32 MB —
// enough for the JIT'd code + audio scratch buffers plus headroom for NAudio's
// and the GC's reserves) tells Windows "you may not trim below this floor".
// Released cleanly when priority mode is turned off.
private const long MinWorkingSetBytes = 32L * 1024 * 1024;
private static void TryLockWorkingSetMin(Action<string>? log)
{
try
{
// dwMaximumWorkingSetSize = -1 means "leave unchanged".
var min = (IntPtr)MinWorkingSetBytes;
var maxUnchanged = (IntPtr)(-1);
if (!SetProcessWorkingSetSizeEx(GetCurrentProcess(), min, maxUnchanged, QUOTA_LIMITS_HARDWS_MIN_ENABLE))
{
log?.Invoke($"priority mode: SetProcessWorkingSetSizeEx(lock) failed (win32={Marshal.GetLastWin32Error()})");
}
}
catch (Exception ex)
{
log?.Invoke($"priority mode: working-set lock threw: {ex.GetType().Name}: {ex.Message}");
}
}
private static void TryRelaxWorkingSet(Action<string>? log)
{
try
{
var unchanged = (IntPtr)(-1);
if (!SetProcessWorkingSetSizeEx(GetCurrentProcess(), unchanged, unchanged, QUOTA_LIMITS_HARDWS_MIN_DISABLE))
{
log?.Invoke($"priority mode: SetProcessWorkingSetSizeEx(unlock) failed (win32={Marshal.GetLastWin32Error()})");
}
}
catch (Exception ex)
{
log?.Invoke($"priority mode: working-set unlock threw: {ex.GetType().Name}: {ex.Message}");
}
}
// === Native interop ===
private const int ProcessMemoryPriority = 0; // PROCESS_INFORMATION_CLASS.ProcessMemoryPriority
private const int ProcessPowerThrottling = 4; // PROCESS_INFORMATION_CLASS.ProcessPowerThrottling
private const uint ProcessPowerThrottlingCurrentVersion = 1;
private const uint ProcessPowerThrottlingExecutionSpeed = 0x1;
private const uint ProcessPowerThrottlingIgnoreTimerResolution = 0x4;
private const int PowerRequestExecutionRequired = 3;
private const uint PowerRequestContextVersion = 0;
private const uint PowerRequestContextSimpleString = 0x1;
private const uint ES_CONTINUOUS = 0x80000000;
private const uint ES_SYSTEM_REQUIRED = 0x00000001;
private const uint MemoryPriorityNormal = 5;
private const int QUOTA_LIMITS_HARDWS_MIN_ENABLE = 0x1;
private const int QUOTA_LIMITS_HARDWS_MIN_DISABLE = 0x2;
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_POWER_THROTTLING_STATE
{
public uint Version;
public uint ControlMask;
public uint StateMask;
}
[StructLayout(LayoutKind.Sequential)]
private struct MEMORY_PRIORITY_INFORMATION
{
public uint MemoryPriority;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct REASON_CONTEXT
{
public uint Version;
public uint Flags;
[MarshalAs(UnmanagedType.LPWStr)]
public string Reason;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetProcessInformation(IntPtr hProcess, int ProcessInformationClass,
ref PROCESS_POWER_THROTTLING_STATE ProcessInformation, int ProcessInformationSize);
// Same kernel32 entry point as SetProcessInformation, but with a different struct type
// in the ref parameter — declare a separate P/Invoke so the marshaller picks the right
// signature without us needing to manually pin and StructureToPtr the buffer.
[DllImport("kernel32.dll", SetLastError = true, EntryPoint = "SetProcessInformation")]
private static extern bool SetProcessInformationMemory(IntPtr hProcess, int ProcessInformationClass,
ref MEMORY_PRIORITY_INFORMATION ProcessInformation, int ProcessInformationSize);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetProcessWorkingSetSizeEx(IntPtr hProcess,
IntPtr dwMinimumWorkingSetSize, IntPtr dwMaximumWorkingSetSize, int Flags);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr PowerCreateRequest(ref REASON_CONTEXT Context);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool PowerSetRequest(IntPtr PowerRequest, int RequestType);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool PowerClearRequest(IntPtr PowerRequest, int RequestType);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
[DllImport("winmm.dll")]
private static extern uint timeBeginPeriod(uint uMilliseconds);
[DllImport("winmm.dll")]
private static extern uint timeEndPeriod(uint uMilliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint SetThreadExecutionState(uint esFlags);
}
+5 -1
View File
@@ -201,8 +201,12 @@ internal sealed class PreferencesDialog : Form
loggingBox.Checked = getLoggingEnabled();
loggingBox.CheckedChanged += (_, _) =>
{
// Machine-local setting (AppConfig.LoggingEnabled). applyLoggingEnabled writes
// through immediately and flips the live gate, so closing the dialog needs no
// further action. NOT a profile setting — do NOT touch ChangedAnyProfileSetting
// or we'll trigger a spurious "save profile?" prompt on exit when the user
// toggled nothing else.
applyLoggingEnabled(loggingBox.Checked);
ChangedAnyProfileSetting = true;
};
writeLogsNowButton.Click += (_, _) => writeLogsNow();