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:
co-authored by
Claude Opus 4.7
parent
146e76c6e6
commit
768875cebb
@@ -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);
|
||||
}
|
||||
@@ -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 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; }
|
||||
|
||||
@@ -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)";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user