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
+147
View File
@@ -0,0 +1,147 @@
using System.Net.Sockets;
using System.Runtime.InteropServices;
namespace RemSound.Core;
/// <summary>
/// qWAVE flow attachment for RemSound's outbound UDP socket. Asks Windows' built-in QoS
/// service (Quality Windows Audio/Video Experience, <c>qwave.dll</c>) to prioritise our
/// audio packets ahead of best-effort traffic on the local hop.
///
/// What this actually buys us:
/// <list type="bullet">
/// <item>The NIC scheduler sends our packets first when there's contention with other
/// outbound traffic (browser downloads, video uploads, OS-level background
/// transfers). Locally, our audio always goes first.</item>
/// <item>Windows marks the outbound packets with DSCP bits (Voice = 46/EF) that user-mode
/// code is normally not allowed to set. The marking propagates across routers that
/// honour DSCP (most consumer kit on a LAN does).</item>
/// <item>On Wi-Fi, the DSCP marking maps to WMM Voice access category — gives our packets
/// the shortest medium-contention window the standard defines. Real win on a busy
/// access point with other Wi-Fi clients fighting for airtime.</item>
/// </list>
///
/// What it doesn't buy us: priority across the public internet. Most ISPs strip or rewrite
/// DSCP at the network edge, so qWAVE markings rarely survive past your local ISP's first
/// hop. LAN and same-house Wi-Fi: tangible benefit. Across the internet: neutral. We do
/// this anyway because the LAN/Wi-Fi half is genuine and there's no cost to attaching the
/// flow on every launch — always-on.
///
/// Lifecycle: attach to the socket once after it's bound, detach on Dispose. The qWAVE
/// flow handle is per-process-per-socket; closing it cleanly releases the OS-side flow
/// state. If anything fails (qwave.dll missing on a stripped-down Windows image, QoS
/// service disabled), the methods log and return false — the socket continues to work
/// without prioritisation.
/// </summary>
public sealed class NetworkPriority : IDisposable
{
private IntPtr qosHandle = IntPtr.Zero;
private uint flowId;
private Socket? attachedSocket;
private bool flowAdded;
/// <summary>Attach the supplied socket to a Voice-priority qWAVE flow. Returns true on
/// success. On failure (logged via <paramref name="onDiagnostic"/>) the socket is
/// untouched and continues to work without prioritisation — the caller does not need
/// to special-case the failure path. The socket must already be bound.</summary>
public bool TryAttach(Socket socket, Action<string>? onDiagnostic = null)
{
if (attachedSocket is not null) return true; // already attached
try
{
var version = new QOS_VERSION { MajorVersion = 1, MinorVersion = 0 };
if (!QOSCreateHandle(ref version, out qosHandle))
{
var err = Marshal.GetLastWin32Error();
onDiagnostic?.Invoke($"qwave: QOSCreateHandle failed (win32={err})");
qosHandle = IntPtr.Zero;
return false;
}
// No DestAddr (IntPtr.Zero) — flow applies to any destination from this socket.
// QOS_NON_ADAPTIVE_FLOW = don't let qWAVE adjust our priority downward if it
// thinks the link is congested; we want consistent Voice priority always.
uint id = 0;
if (!QOSAddSocketToFlow(qosHandle, socket.Handle, IntPtr.Zero,
QOS_TRAFFIC_TYPE_VOICE, QOS_NON_ADAPTIVE_FLOW, ref id))
{
var err = Marshal.GetLastWin32Error();
onDiagnostic?.Invoke($"qwave: QOSAddSocketToFlow failed (win32={err})");
QOSCloseHandle(qosHandle);
qosHandle = IntPtr.Zero;
return false;
}
flowId = id;
attachedSocket = socket;
flowAdded = true;
onDiagnostic?.Invoke($"qwave: attached send socket to Voice-priority flow (flowId={flowId})");
return true;
}
catch (Exception ex)
{
onDiagnostic?.Invoke($"qwave: attach threw {ex.GetType().Name}: {ex.Message}");
if (qosHandle != IntPtr.Zero)
{
try { QOSCloseHandle(qosHandle); } catch { /* ignore */ }
qosHandle = IntPtr.Zero;
}
return false;
}
}
public void Dispose()
{
try
{
if (flowAdded && attachedSocket is not null && qosHandle != IntPtr.Zero)
{
QOSRemoveSocketFromFlow(qosHandle, attachedSocket.Handle, flowId, 0);
}
if (qosHandle != IntPtr.Zero)
{
QOSCloseHandle(qosHandle);
}
}
catch { /* shutdown is best-effort */ }
finally
{
flowAdded = false;
attachedSocket = null;
qosHandle = IntPtr.Zero;
flowId = 0;
}
}
// === Native interop ===
// qWAVE traffic-type ordering (higher = more priority): BestEffort < Background <
// ExcellentEffort < AudioVideo < Voice < Control. We pick Voice rather than
// AudioVideo because Voice has the most aggressive jitter requirements in qWAVE's
// model, which matches RemSound's sub-50 ms expectations.
private const int QOS_TRAFFIC_TYPE_VOICE = 4;
private const uint QOS_NON_ADAPTIVE_FLOW = 0x2;
[StructLayout(LayoutKind.Sequential)]
private struct QOS_VERSION
{
public ushort MajorVersion;
public ushort MinorVersion;
}
[DllImport("qwave.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool QOSCreateHandle(ref QOS_VERSION Version, out IntPtr QOSHandle);
[DllImport("qwave.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool QOSCloseHandle(IntPtr QOSHandle);
[DllImport("qwave.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool QOSAddSocketToFlow(IntPtr QOSHandle, IntPtr Socket,
IntPtr DestAddr, int TrafficType, uint Flags, ref uint FlowID);
[DllImport("qwave.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool QOSRemoveSocketFromFlow(IntPtr QOSHandle, IntPtr Socket,
uint FlowID, uint Flags);
}
+11 -2
View File
@@ -52,8 +52,17 @@ public sealed class Profile
public int OpusFrameMilliseconds { get; set; } = 10;
public int SendRateRaw { get; set; } = (int)SendRate.Standard;
public bool TightLatencyMode { get; set; }
/// <summary>True suppresses the connect/disconnect sound cues. Off by default.
/// 2026-05-06.</summary>
/// <summary>True if this profile asks Windows to keep the RemSound process in
/// high-priority mode while it's running — CPU scheduling, power management, memory
/// priority, working-set lock, and MMCSS thread priority all elevated. Off by default;
/// the user opts in per profile when they want a "live session" feel where the
/// cold-start CPU ramp doesn't audibly hurt latency. On laptops that drains the
/// battery faster; on desktops it costs a couple of extra watts. See
/// <c>PerformanceMode</c> in the App project for the full lever list. Saved per
/// profile (not in AppConfig) because the right answer genuinely differs between
/// profiles.</summary>
public bool PriorityMode { get; set; }
/// <summary>True suppresses the connect/disconnect sound cues. Off by default.</summary>
public bool MuteConnectionCues { get; set; }
public int MaxLatencyMs { get; set; } = 80;
public int Smoothness { get; set; } = 3;
@@ -284,6 +284,21 @@ public sealed class RemSoundSettingsStore
Save(s);
}
/// <summary>Priority mode for the current profile. When true, the App's
/// <c>PerformanceMode</c> helper drives every Win32 lever that elevates a process's
/// scheduling and memory behaviour: EcoQoS opt-out, kernel power request, High
/// priority class, 1&nbsp;ms scheduler quantum, memory priority, working-set lock,
/// and MMCSS thread priority. Off by default; the user opts in per profile.</summary>
public bool LoadPriorityMode(bool defaultValue = false) =>
Try(() => Load()?.PriorityMode) ?? defaultValue;
public void SavePriorityMode(bool value)
{
var s = Load() ?? new Settings();
s.PriorityMode = value;
Save(s);
}
/// <summary>Suppresses the connect/disconnect sound cues that play when a peer's health
/// transitions to/from Healthy. Off by default — cues are on. Saved per-profile so users
/// who don't want them in a given setup don't have to remember to mute every session.
@@ -399,6 +414,7 @@ public sealed class RemSoundSettingsStore
// mode is derived from AsioDriverName and the Both-mode warning popup is gone.
SendRate = profile.SendRate,
TightLatencyMode = profile.TightLatencyMode,
PriorityMode = profile.PriorityMode,
Smoothness = profile.Smoothness,
ConcealmentArtifact = (ConcealmentArtifact)profile.ConcealmentArtifactRaw,
MuteConnectionCues = profile.MuteConnectionCues,
@@ -439,6 +455,7 @@ public sealed class RemSoundSettingsStore
// popup that owned the suppression flag is gone.
if (s.SendRate is SendRate sr) profile.SendRate = sr;
if (s.TightLatencyMode is bool tl) profile.TightLatencyMode = tl;
if (s.PriorityMode is bool pm) profile.PriorityMode = pm;
if (s.Smoothness is int sm) profile.Smoothness = sm;
if (s.ConcealmentArtifact is ConcealmentArtifact ca) profile.ConcealmentArtifactRaw = (int)ca;
if (s.MuteConnectionCues is bool mc) profile.MuteConnectionCues = mc;
@@ -492,6 +509,7 @@ public sealed class RemSoundSettingsStore
// derived from AsioDriverName via LoadAudioMode; the Both-mode warning popup is gone.
public SendRate? SendRate { get; set; }
public bool? TightLatencyMode { get; set; }
public bool? PriorityMode { get; set; }
public int? Smoothness { get; set; }
public ConcealmentArtifact? ConcealmentArtifact { get; set; }
public bool? MuteConnectionCues { get; set; }
+8 -2
View File
@@ -30,8 +30,14 @@ public sealed class WindowsAudioThreadBoost : IDisposable
if (avrtHandle != IntPtr.Zero)
{
AvSetMmThreadPriority(avrtHandle, AvrtPriority.High);
Mode = $"MMCSS {taskName}";
// Critical sits one notch above the MMCSS task's default priority (High).
// It's still well below RealTime — the OS keeps a reservation for system
// services above us so the audio stack and screen reader can't be starved.
// Empirically the bump helps the receive listener and the render thread when
// the machine is also doing other foreground work (browser, NVDA reading a
// page) by getting us off the queue ahead of those tasks' worker threads.
AvSetMmThreadPriority(avrtHandle, AvrtPriority.Critical);
Mode = $"MMCSS {taskName} (priority Critical)";
}
}