Skip to content

Commit 7939d12

Browse files
committed
Profile switch: apply per-slot HM profile slug + transition MapTo without reset window
Two adjacent bugs in InputService.ApplyProfile, the runtime profile- switch path: 1. SlotProfileIds was never applied. Switching profiles re-set SlotCreated, SlotControllerTypes, ExtendedConfigs, MidiConfigs, slot orders, and DSU/web/overlay state, but never touched _mainVm.Pads[i].ProfileId. The HM profile slug stuck at whatever the previous profile last set, so a slot configured for xbox-one-s-bt in Profile A stayed on xbox-one-s-bt after switching to Profile B even when B explicitly saved xbox-series-xs-bt for that slot. Step 5's per-slot diff at InputManager.Step5.VirtualDevices.cs:514-527 was already wired to destroy + recreate slots whose desired SlotProfileIds[i] differs from the live HMaestroVirtualController.ProfileId, but it never fired because the desired array wasn't being updated. Add the apply inside the topology loop, gated on willCreate so destroyed slots don't pick up stale slugs. 2. Device-assignment apply used a reset-then-rebuild shape: every us.MapTo set to -1, then reapply from profile.Entries. The lock spans both phases but Step 5's IsSlotActive / HasAnyDeviceMapped reads don't take that lock — they iterate UserSettings.Items under a separate sync. So the polling thread can observe the reset window and fall into the immediate-destroy path at Step5:590-600 (slot inactive AND no devices mapped → destroy now, no grace counter, no inactivity timeout). Slots whose mapping is identical between old and new profile still get destroyed and recreated, including kernel-slot reallocation and the bubble-up cascade. Replace with a single-pass transition: build the desired final assignment map from profile.Entries first, then transition each UserSetting directly old→new MapTo (or →-1 for entries dropped from the new profile). The "find an unassigned UserSetting" gate switches from MapTo<0 (which required the reset) to "not yet consumed by a prior entry in this same apply pass." A device mapped to multiple slots in the new profile still claims one UserSetting per entry. Net behavior: switching profiles now correctly switches HM identity on every slot whose stored profile slug differs, with pointer- survival on slots that share the same slug (Step 5's existing diff handles this — no new logic). Slots whose device mapping is unchanged across profiles transition without any teardown at all, where v3.0.1 would tear them down spuriously.
1 parent 8dedb6b commit 7939d12

1 file changed

Lines changed: 55 additions & 16 deletions

File tree

PadForge.App/Services/InputService.cs

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3575,40 +3575,68 @@ public void ApplyProfile(ProfileData profile)
35753575
SettingsManager.SlotEnabled[i] = (profile.SlotEnabled != null && i < profile.SlotEnabled.Length)
35763576
? profile.SlotEnabled[i]
35773577
: willCreate;
3578+
3579+
// HM profile slug. Step 5's per-slot diff
3580+
// (InputManager.Step5.VirtualDevices.cs:514-527) reads this
3581+
// via _inputManager.SlotProfileIds[i] and only destroys +
3582+
// recreates the live VC when the new slug differs from the
3583+
// current HMaestroVirtualController.ProfileId. Slots whose
3584+
// HM slug matches across profiles stay live, pointer-stable.
3585+
// Skipping this apply leaves the slot stuck on the previous
3586+
// profile's slug, so the HM identity never switches.
3587+
if (willCreate
3588+
&& profile.SlotProfileIds != null
3589+
&& i < profile.SlotProfileIds.Length)
3590+
{
3591+
_mainVm.Pads[i].ProfileId = profile.SlotProfileIds[i];
3592+
}
35783593
}
35793594
}
35803595

3581-
// ── Reset all device assignments, then apply profile entries ──
3582-
// Each profile fully owns slot assignments; unassign everything
3583-
// first so devices not in this profile don't leak from the previous one.
3596+
// ── Single-pass transition of device assignments ──
3597+
// Each profile fully owns slot assignments. Avoid the reset-then-
3598+
// rebuild shape (set every us.MapTo = -1, then reapply from
3599+
// profile.Entries) — that opens a window where the polling thread
3600+
// sees HasAnyDeviceMapped == false for surviving slots and falls
3601+
// into the immediate-destroy path at
3602+
// InputManager.Step5.VirtualDevices.cs:590-600, tearing down VCs
3603+
// that should survive the switch (slots whose mapping is unchanged
3604+
// between old and new profile would still get destroyed and
3605+
// recreated needlessly, including kernel-slot reallocation and the
3606+
// bubble-up cascade).
3607+
//
3608+
// Build the desired final assignment map first, then transition
3609+
// each UserSetting directly: old → new MapTo for entries that
3610+
// survive, or → -1 for entries dropped from the new profile.
35843611
lock (SettingsManager.UserSettings.SyncRoot)
35853612
{
3586-
foreach (var us in SettingsManager.UserSettings.Items)
3587-
us.MapTo = -1;
3613+
var assignments = new System.Collections.Generic.Dictionary<UserSetting, (int MapTo, PadSetting Ps)>();
3614+
var consumed = new System.Collections.Generic.HashSet<UserSetting>();
35883615

35893616
if (profile.Entries != null && profile.Entries.Length > 0 &&
35903617
profile.PadSettings != null && profile.PadSettings.Length > 0)
35913618
{
35923619
foreach (var entry in profile.Entries)
35933620
{
3594-
// Find the PadSetting template by checksum first — skip if missing.
35953621
var template = profile.PadSettings
35963622
.FirstOrDefault(p => p.PadSettingChecksum == entry.PadSettingChecksum);
35973623
if (template == null) continue;
35983624

3599-
// Find an UNASSIGNED UserSetting for this device.
3600-
// A device mapped to multiple slots has multiple profile entries;
3601-
// each must claim a separate UserSetting (MapTo < 0 = unclaimed).
3625+
// Find a UserSetting for this entry, gated on
3626+
// "not yet consumed by a prior entry in this same
3627+
// apply pass" rather than the old MapTo<0 check —
3628+
// that check required the bulk reset we're avoiding.
3629+
// A device mapped to multiple slots in the new profile
3630+
// still claims one UserSetting per entry.
36023631
var us = SettingsManager.UserSettings.Items
3603-
.FirstOrDefault(s => s.InstanceGuid == entry.InstanceGuid && s.MapTo < 0);
3632+
.FirstOrDefault(s => s.InstanceGuid == entry.InstanceGuid && !consumed.Contains(s));
36043633

36053634
if (us == null && entry.ProductGuid != Guid.Empty)
36063635
{
36073636
us = SettingsManager.UserSettings.Items
3608-
.FirstOrDefault(s => s.ProductGuid == entry.ProductGuid && s.MapTo < 0);
3637+
.FirstOrDefault(s => s.ProductGuid == entry.ProductGuid && !consumed.Contains(s));
36093638
}
36103639

3611-
// No unclaimed UserSetting found — create one for this slot.
36123640
if (us == null)
36133641
{
36143642
us = new UserSetting
@@ -3619,10 +3647,21 @@ public void ApplyProfile(ProfileData profile)
36193647
SettingsManager.UserSettings.Items.Add(us);
36203648
}
36213649

3622-
// Clone and apply PadSetting + slot assignment.
3623-
var ps = template.CloneDeep();
3624-
us.SetPadSetting(ps);
3625-
us.MapTo = entry.MapTo;
3650+
consumed.Add(us);
3651+
assignments[us] = (entry.MapTo, template.CloneDeep());
3652+
}
3653+
}
3654+
3655+
foreach (var us in SettingsManager.UserSettings.Items)
3656+
{
3657+
if (assignments.TryGetValue(us, out var assign))
3658+
{
3659+
us.SetPadSetting(assign.Ps);
3660+
us.MapTo = assign.MapTo;
3661+
}
3662+
else if (us.MapTo >= 0)
3663+
{
3664+
us.MapTo = -1;
36263665
}
36273666
}
36283667
}

0 commit comments

Comments
 (0)