2.0 Update - #74
Open
NightHammer1000 wants to merge 805 commits into
Open
2.0 Update#74NightHammer1000 wants to merge 805 commits into
NightHammer1000 wants to merge 805 commits into
Conversation
Bump the library to the commit that exposes S0 low-power-idle capability, the actionable wake sources (programmable, plus armed-but-fixed), per-device arming that re-reads programmability before it writes, and snapshot/restore. WSGM consumes none of it yet. Higher-level Modern Standby policy and its UI are the separate WSGM Core work; the library stays policy neutral and the power button, sleep button and lid are reported rather than writable. Closes #26
Bump the library to the commit that reports whether Windows attributes the last resume to the user or to the machine, and the interrupt-time marks around the last standby. This is the primitive the Modern Standby enhancement in #27 is built on. The investigation there found that Winhanced changes no power settings and no wake sources at all: it lets the machine wake, asks IsSystemResumeAutomatic whether a person did it, and suspends again when nobody did. That leaves nothing global altered, so there is nothing to restore after a crash, which is why WSGM follows it rather than Handheld Companion's eight scheme writes. WSGM consumes none of it yet. Refs #27
Core\HybridCores consumes the Windows Device Control APIs added for #35 and owns the policy: which modes a machine actually accepts, what each is called in terms of the handheld rather than the scheduler, the write, and the readback that confirms it. The overlay puts it on Device > Power beside the Windows energy plan, where Windows power policy belongs and where it stays reachable with device integration off. Steam gets the same control as a Performance dropdown through the toolkit row. Both surfaces hold the same policy object and the same id vocabulary, so a change from either is one write and the next read on the other reports it. Neither caches a value: activating a power scheme can carry a different preference with it, and a remembered one would report a placement Windows had already replaced. WSGM writes only the two thread scheduling settings Windows names itself. It reads and carries through the heterogeneous-policy value beside them without ever choosing one, because powercfg enumerates that setting as "use heterogeneous policy 0..4" and publishes no meaning for the numbers. Handheld Companion assigns them meanings; that is an assumption, and #35's rule is no undocumented write without a verified readback. A stored pair that disagrees with itself, or holds a value WSGM does not offer, reads back as no mode at all rather than the nearest one. Something else set it, and showing it as a WSGM mode would claim WSGM had. The control is hidden outright on a CPU with one efficiency class. Closes #36
The Modern Standby enhancement mode #27 asks for, in the shape the Winhanced investigation found rather than Handheld Companion's. WSGM changes no power settings and arms no wake sources: the machine wakes normally and the guard only decides whether to put it back. Nothing global is left altered, so a WSGM that dies mid-session leaves nothing to restore, and switching the mode off is the whole of turning it off. IsSystemResumeAutomatic is the basis. Windows says whether a person woke the machine, and a wake it attributes to the user is never undone. The display gate is the one that does not rely on Windows counting a device as input. A gamepad does not advance the last-input time, which is the same fact behind #69, so a player holding a controller reads as idle by that measure alone. Suspending a machine under someone's hands is the one failure this must never produce, and a dark screen is the evidence nobody is looking. Every other refusal follows from the same asymmetry: staying awake is something the user can recover from, and suspending on a state WSGM could not establish is not. A read that throws leaves the machine awake. A wake is slept through at most three times, so a machine waking for a cause WSGM cannot see is never suspended in a loop the user cannot escape. Only the settle period is worth waiting through; every other refusal is a fact about the wake that will not change while it lasts, so the timer stops rather than polling forever. Off by default and switched on from Settings > System > Power, because it decides on its own to suspend the machine. Refs #27
The badge sat in the viewport's top-left corner over Steam's search bar, was 14px, said nothing about the internal library, and never said whether the card it named was still in the machine. It now anchors under the hero art's bottom-left, which is the block that describes the game and the element the app-id detection already proves it can find on device, so the badge travels with the art instead of covering Steam's own controls. A page with no hero art keeps the old corner: worse placement, but still an answer. The internal library is named from the ABSENCE of a map entry rather than by pushing every internal app id into the page. That is why the connected flag is always emitted: the script tells no-entry from entry-disconnected, and a key omitted when false would collapse the two into one. Hidden cards are pushed now as well, because hiding governs the tab and not where the game is, and leaving them out made those games read as internal. Disconnected is carried by a hollow ring and the word, not by colour alone. The name survives disconnection because it comes from the card's own marker, which is also why it is untrusted text and goes through JsString before it is spliced into the script. Closes #28
The capture path was wired end to end and had no test at all: the projection carried InputVolumePercent, InputMuted and ActiveInputDeviceId to Steam's audio store, and nothing proved the microphone stayed separate from the speakers. An absent capture endpoint publishes no microphone volume rather than zero. Zero is a volume somebody can set, so publishing it for a machine with no microphone would render a real slider sitting at silent. ApplyInputVolume becomes internal to make this testable. It records what Windows reported; the InputVolumePercent setter is the user's path and queues a hardware write, so a test driving that would have changed the volume on the machine running the suite.
A fresh install booted to the framework's own field defaults, which is what the Classic preset is: black, a 26px Please wait, a white ring. That is a configurable system with nothing chosen, not a product's first impression. The shipped preset is built from the application's own tokens rather than a separate splash palette, so the first thing the machine shows already looks like the rest of WSGM. A sweep along the bottom edge rather than a ring beside the wordmark: startup has no steps to report, and a line travelling an edge reads as working without competing with the only thing on screen worth reading. The ground is a hair above pure black because an OLED handheld shows panel mottling on #000000 and the vignette needs somewhere to fall off to. The caption is the one measurable change rather than a taste one. The older presets set it at #5F5F5F, roughly 3:1 on black, for the line that has to carry a startup failure at arm's length on a seven-inch panel. It now uses the application's muted text token, above 11:1. Only a NEW configuration is seeded with it. One that already carries a splash section keeps whatever it says, so nobody's chosen look changes on upgrade. That distinction had a bug in it: Normalize repairs an individual null or unreadable splash field to new AppConfig().Splash, so once that carried the 2.0 preset, a Classic splash with one bad field would have come back wearing part of 2.0's look. Field repair now targets SplashConfig's own defaults. Seeding a new configuration and repairing one field of an existing one are different questions and only the first is about what WSGM ships with. No image assets: a preset never fabricates one, and a default that depended on a file could not survive that file being missing. Refs #40
The tracker is the progress record the contributor guide points at, and it still said 28 issues were open and named none of #20, #22, #26, #28, #35 or #36. Reconciling it is required before each of those commits and I did not do it, so it is one entry per issue here rather than eight amendments. Each entry says what it does, what was actually verified and on what, and what was not. The counts come from the issue list rather than from arithmetic on the old ones. The header now also records why the count stops at 15 for 2.0: every remaining issue waits on a maintainer decision, a live Steam session, the hardware, an attended measurement, an organization fork, or credentials. Written down so the next person does not re-derive it, and so a stalled count is not mistaken for a stalled workoff.
The graphics driver is on every machine this plugin runs on, so the plugin calls it, exactly as variable refresh already does. ArcSyncTransport has been loading ControlLib.dll by name and holding an IGCL session since August; this adds ctlGetSet3DFeature beside it rather than anything new. Two GenericChoice capabilities under Power: control, off/on/auto, and the frame target, performance/balanced/battery. Generic because the SDK already carries that role, so a device-specific driver feature needs no contract change. Support is decided by a successful read rather than by walking ctlGetSupported3DCapabilities. A read is the only claim worth making, it is the state the row publishes anyway, and it avoids marshalling a capability array whose union sizing is one more layout to get wrong for no extra certainty. The driver holds control and target together, so either row's write carries the other's current value read back fresh rather than a remembered one. Writes are verified by read-back and never retried. Restore puts back only what was captured at acquire, and only when it actually moved. Layouts come from Intel's published igcl_api.h rather than inference. Verified on the reference Claw 8 AI+ A2VM: ctl_3d_feature_getset_t is 56 bytes, ctlInit reports IGCL 1.1, one adapter enumerates, and Endurance Gaming reads back control=Off mode=Performance. A second concurrent IGCL session alongside ArcSync's was confirmed to work rather than assumed. One trap found by the existing tests: a capability absent from ServiceForCapability publishes a descriptor and never a state, so the rows would have rendered as a driver fault. Both ids are mapped there and in the firmware kind map, which is None for these — nothing is written into firmware and the journal has nothing to put back. Closes #37
This plugin's guidance requires a provenance entry for any display or IGCL claim and a test that pins the native sizes, and ArcSyncLayoutTests already set that precedent. The previous commit shipped neither. The provenance also draws a line the commit message blurred. Only the read is device-verified: ctlInit at IGCL 1.1, one adapter, EGControl=OFF and EGMode=PERFORMANCE off the reference unit. No value was applied to the hardware, so the write and its read-back remain a source-and-layout claim awaiting an attended Device Lab run, and it says so rather than reading as a full pass. The layout test exists for the same reason the Arc Sync one does: the driver checks the Size field, and its refusal is indistinguishable from the machine not having the feature, so drift removes the row silently.
Same transport, same library, one more 3D feature. It is bool-typed rather than custom, so the value rides in the property union with CTL_PROPERTY_VALUE_TYPE_BOOL instead of through pCustomValue, and only the union's first byte carries it because ctl_property_boolean_t is a single C bool. The transport is renamed to match what it now covers. Each feature is probed on its own at acquire: a driver can answer for one and not another, and a missing feature must not cost the device the rows for the rest. Verified on the reference unit on 2026-09-10: feature 18 answers and reads back enabled. Only the read is device-verified; no value was applied, and the provenance says so. The same probe found CTL_3D_FEATURE_GAMING_FLIP_MODES and CTL_3D_FEATURE_LOW_LATENCY also answer. They are recorded as observed-present and unimplemented. Neither is a driver-level VSync toggle, and Intel's header has no VSync feature at all, which is a finding #64 needs. Closes #33
Windows feeds its idle timer from the raw keyboard and mouse stream and a HID gamepad is not part of it, so a controller-only session looks idle however hard it is being played. Measured on the reference handheld with Big Picture in the foreground: powercfg /requests listed a DISPLAY request from RustDesk and from nothing else. Neither Steam nor the title registers one. The display idle timeout reads 60 seconds on both power sources. Nothing was preventing the screen going dark on top of a running game, which is the whole of #69 and has nothing to do with the Steam Input Lease it suspected. GameplayDisplayHold takes a DISPLAY request while a game is running and drops it when one is not, driven from the same running-application snapshot AutoTDP uses. Scoped to the application and never to WSGM's lifetime, so it cannot become a permanent keep-awake, and disposal releases it so a session torn down mid-game cannot strand the request on a process that is gone. DISPLAY rather than SYSTEM: the complaint is the screen, and stopping standby as well would take a decision away from the user that this is not entitled to make. The Power tab's manual Keep Awake is separate and unaffected. Closes #69
Intel's Shared GPU Memory Override decides how much system memory the integrated GPU may use. It is not in IGCL: ControlLib.dll has four memory entry points and every one of them is a get, which is what my first answer on the issue mistook for "no programmatic control exists". The driver reads a percentage from GpuSystemMemoryPinninglimit under the display adapter's GMM key when it initialises its memory manager, which is why the change only takes effect after a restart. Established on the reference unit by driving Intel Graphics Software and watching what moved. At rest the value read 57, Intel's documented default, and the adapter reported 19,327,352,832 bytes against 33,866,657,792 of total physical memory - 57.07% of it, which is what ties the value to the feature. Setting the panel to 44% wrote 44 into exactly that value and nothing else: no other value under the adapter, nothing under HKLM or HKCU SOFTWARE\Intel, nothing in ProgramData. Pressing reset wrote 57 back rather than deleting the value, so the default is the literal 57 and there is no separate "changed" flag. An absent value therefore reads as the default rather than as a missing feature, so an untouched machine still gets the control. qwMemorySize did not move across either change, which is the restart requirement showing itself. The adapter is matched rather than hard-coded. Its index is 0001 here and the class also holds an unreadable 0000 beside it, so the search contains a per-subkey access failure instead of abandoning the enumeration, prefers an adapter that already carries the value, and refuses when more than one candidate remains. The offered range is 13-87 percent, which is what Intel Graphics Software shows. The setting is a persistent user choice like the charge limit, not a resource the plugin borrows, so it is neither journalled nor restored on stop. Only the write is verified; the split itself needs a restart and the row's label says so. Eighteen focused tests cover the gates, the resolver, the range and the write against a disposable HKCU subtree. Closes #32
Picks up the left slideout navigation panel extension surface: SteamNavigationPanelSurface, the navigationPanel gate, and the emitted-asset harness that exercises its descent, anchoring, hiding and restoration. Refs #23
Picks up SteamPageSurface and the pages gate: custom routes registered with Steam's own router, built with its back-stack Route so they keep native back navigation, with override and addition distinguished under the first-match switch. Refs #24
The Artwork Searcher now asks every configured provider through one contract instead of calling SteamGridDB directly. SteamGridDB keeps every endpoint, header and failure message it had; the picker no longer knows which source a result came from beyond labelling it. Providers are searched in parallel rather than with fallback priority. Fallback would let a slow or empty primary hide a good secondary result, and the acceptance this was written against is that one provider's failure does not break another - which only holds if the others were actually asked. Declaration order decides ties, so SteamGridDB still leads, and results are de-duplicated by URL because the same asset genuinely comes back from both. The contract carries provider readiness as a first-class state, which is the part that matters. A source that was never asked and a source that was asked and found nothing both produce an empty grid, so every refusal is named on screen: a rate limit, a missing credential, a provider switched off. An empty result can no longer read as "this game has no artwork". The two sources differ in exactly the ways that shaped the contract. SteamGridDB takes a free personal key and can be addressed by Steam app id. Screenscraper issues developer credentials per application, so WSGM cannot ship any and the user supplies their own; it indexes emulated systems by ROM, so a Steam app id means nothing to it and it answers title searches only - reporting nothing rather than returning whatever game shares the number. Its media vocabulary and its world-region-first preference stay inside the provider, as do its documented quota failures: 429 is the thread or minute quota, 430 the daily one. Applying is unchanged and provider-independent: one SetCustomArtworkForApp call, whichever source supplied the bytes. Screenscraper's API shape was taken from its own published documentation rather than inferred. Nine focused tests cover readiness for both providers, the skip-versus- failure distinction, a Steam app id being refused by the ROM-indexed source, and a match only ever being asked of the provider that issued it. 2345 WSGM tests pass. Not verified: no live Screenscraper request has been made, because that needs developer credentials this machine does not have. The provider reports exactly that state rather than failing silently. Closes #46
The re-suspend policy, the guard and the Settings toggle already shipped from the #27 investigation. What was missing was the diagnostic half of it: the issue asks for "useful diagnostics showing why the last standby ended or what woke the machine where possible", and the feature offered none. ModernStandbyDiagnostics reads Windows' own account for a row under the toggle. It reports whether the machine does S0 low-power idle at all - so one that cannot use the feature is told rather than offered it - how long the last standby lasted, how long it has been awake, and which devices are currently allowed to wake it. It deliberately never names what woke the machine. Windows exposes no documented call for that, and printing a cause inferred from timings would be a guess presented as a diagnosis. The attribution is reported exactly as far as it goes: whether the resume was put down to a person. The armed-device list is the actionable half. Read off the reference handheld: two of three wake-capable devices are armed, the Intel Wi-Fi 7 BE201 and the USB4 root router, after a 22.7-hour standby Windows attributed to a person. On a handheld that list is usually the answer to why it came back on in a bag. TrySetWakeArmed could act on it and WSGM deliberately does not, because disarming a wake source is a global change that outlives the process - the one thing this feature's design avoids, so that switching it off leaves nothing to restore. Both reads are best-effort and bounded: a diagnostic that throws would take the settings page with it. Six focused tests cover the duration wording in the user's own culture (this box formats 22.7 with a comma), that reading never throws and always says something, that a supported machine never carries the unsupported wording, and that the summary never names a cause. 2354 WSGM tests pass. Not measured: no battery-drain comparison has been run. Whether the re-suspend and these diagnostics add up to less drain overnight is an attended measurement nobody has done, and this does not claim it. Closes #27
Records the toolkit gitlink that revives SteamOS storage management, and puts WSGM's existing operations behind it. SteamStorageBridge delegates every action to the manager that already owns it: RemovableDriveManager for eject, SdFormatManager for erase and library registration. Nothing is reimplemented for Steam's benefit, so the identity re-checks, the volume-GUID resolution, the backup-and-splice of Steam's own library file and the staged destructive sequence are the same code whichever surface asked. That is the requirement this issue leads with, and the reason a second implementation would have been the wrong answer. Null and empty are kept apart on the wire. Null means "nothing to say" and leaves Steam's page as it was; an empty state asserts the machine has no removable storage and the page renders that. Publishing empty before the managers have scanned would show "no drives" to someone holding a card. Every refusal carries its reason, because the injected side has nowhere to put one and the user would otherwise see a control that did nothing: a drive that has gone, a drive that is busy, one already ejected, a format already running, and trim, which is refused outright rather than faked since neither manager exposes one and the state says so. Formatting stays behind its own switch, off by default. Reviving these pages makes eject and library registration reachable from Big Picture and both are recoverable; erasing a card is not, and Steam's page offers it to a single controller press. The format path itself is unchanged - the switch only decides whether it may be started from Steam - which keeps this repository's rule that erasing storage is attended without pretending the capability is missing. Six focused tests cover the letters-to-mount-paths conversion, including the null and blank cases. The managers themselves are not exercised: they own real disks, and this test project never touches storage. 2360 WSGM tests pass. Not verified: the gate has not been installed into a running Steam, so no storage page has yet rendered and no action has been driven from one. Closes #30 Closes #31
This issue was parked on "Intel's header has no CTL_3D_FEATURE_VSYNC", which was true and beside the point. What Intel has is CTL_3D_FEATURE_GAMING_FLIP_MODES, a flag set whose members are the presentation modes, and forcing VSync on is one of them. Reading the driver's own capability array on the reference unit returns twelve supported features, feature 9 enum-typed with a supported mask of 0x2d: application default, VSync on, Smooth Sync and capped FPS. VSync off is deliberately not in that mask, because leaving it off is what the application default already means - the driver offers forcing sync on, not forcing it off. So the capability is a choice rather than a boolean, and the offered set is read from the driver's supported mask rather than hard-coded, which is the forward compatibility the issue asked for: a driver that adds a mode gets it with no contract change. The capability element layout is not asserted. It is the one IGCL structure this package would otherwise have to guess, and guessing reads garbage, so the stride is derived from the data - the only stride yielding that many distinct in-range feature ids - measured as 72 bytes here. A driver whose layout does not resolve falls back to offering nothing rather than offering something wrong. IGCL answers which modes exist and nothing else. ctlGetSet3DFeature reports feature 9 with an enable byte and a value of zero regardless of what is set, and a write returns CTL_RESULT_SUCCESS while changing nothing observable - not its own getter, not the stored value, not the per-app entries. Tested unelevated and elevated, with Intel Graphics Software and IntelGraphicsSoftwareService running. The mode actually lives in <adapter>\3DKeys\Global_AsyncFlipMode, beside Global_EnduranceGaming and Global_LowLatency, carrying Intel's own flag values, with an untouched machine reading 1 for application default. So the capability reads and writes the driver's own store, exactly as the shared-memory split does, and uses IGCL only to ask what is offered. Confirmed elevated on the reference unit: 1, 4, 8 and 32 each wrote and read back exactly, and the original restored. The write requires elevation because the key is under HKLM\SYSTEM. WSGM has that as a shell replacement; unelevated every write fails cleanly and is reported as failed rather than assumed applied. The same requirement applies to the shared-memory split, whose write had never been exercised on the unit until this work - now it has, through the same path. Not journalled and not restored on stop: like the memory split, this is a persistent user choice the driver keeps. Eight further tests cover storing and reading back each of Intel's flag values, an absent mode reading as nothing rather than zero, the settings key being created when absent, and no adapter meaning no mode. 163 plugin tests pass. Not established: whether the driver acts on the value without a restart. The row's label says so rather than implying an immediate effect. Closes #64
dev-deploy copied WSGM.exe, WSGM.Launch.exe and every DLL, and left WSGM.deps.json alone. The host reads that manifest to decide what may be loaded at all, so a swap that adds an assembly without it produces the worst failure available: the DLL is sitting in the directory and the runtime still reports "Could not load file or assembly", pointing every diagnostic at a file that is plainly there. Hit on the reference Claw tonight, on the first swap after WSGM.Plugin.Sdk became a project reference. WSGM crashed on start five times over; the manifest in bin was dated 2026-09-01, six weeks older than the assembly beside it. Config was never at risk - ConfigStore copies rather than moves, so config.json came through untouched and only left identical config.bad.* copies behind - but every start failed and the shell never came up. runtimeconfig.json travels with it for the same reason: both are generated by the publish and both describe the set that was just copied. Also regenerates the bundled Steam UI asset, which had drifted from its TypeScript source once the navigation-panel, custom-page and storage gates were added to the toolkit. The drift check in dev-deploy caught that before anything was swapped, which is the check working as intended.
The fork existed as a bare mirror of upstream, and WSGM still built corando98/VIIPER with six .patch files applied at build time. That satisfied neither half of what the fork is for: nothing was carried on it, and nothing consumed it. The six patches are now commits on its wsgm branch, each with the reasoning that lived in third_party's README, so the diff is reviewable where it applies, git log attributes it, and refreshing against upstream is one rebase instead of six patches to re-fit by hand. viiper-controller stays an untouched mirror to rebase onto. The build script no longer applies anything. Two changes are new. viiper_device_add_ex still attached, so the duplicate attachment and the impossible frame ordering that made add stop attaching both survived on the extended entry point; WSGM calls add, so this was an inconsistency rather than a live fault. And getDeviceInterfacePath discarded the result of its size query - a query that is *expected* to fail with ERROR_INSUFFICIENT_BUFFER, but whose other failures leave requiredSize zero, at which point the next two lines allocate an empty slice and index element zero of it. Discovery panicked inside the server instead of reporting a missing driver. That one is ported from Alia5's "Solidify autoattach handling" through Valkirie, rewritten because the structure and its callers are renamed on this branch. Everything else from the variants was read and rejected for a stated reason, recorded in third_party\controller\viiper\README.md: Valkirie's attach deduplication keeps add enumerating the device, which is the half WSGM cannot have; its CPU fix adds up to 10 ms to every URB header read and addresses busy-loops the NAK-idle work already removed; the upstream efficiency commit is already on the baseline; the build and CI tooling changes nothing WSGM ships, since it builds clib directly and stages the notices itself; hbashton's scheduling work solves the same problem as NAK-idle through a conflicting model across devices WSGM does not create; and its install hardening is for a script WSGM does not run. The build script also repoints origin before fetching. Without that, moving the pin to a different fork fails as "revision unavailable" on every machine that already has a source tree - which is every developer machine. Verified on the reference Claw: go build ./... clean, go test ./... failing in exactly the three places the baseline already failed and nowhere else, build-viiper.ps1 -Validate staging libviiper.dll from the new pin against the pre-existing checkout, and 171 WSGM controller tests passing. Also drops an unused using left in SteamStorageBridge, which the style gate rejects.
SteamStorageBridge existed, had tests, and was referenced by nothing. No session constructed it and no module declared the surface, so the gate was never registered, never applied, and never reached a running Steam. The feature could not have worked, which is what the commit that added it should have said instead of calling itself unverified. The session now owns the two managers the bridge delegates to, the way it already owns audio and the radios and for the same reason: Steam's pages answer while the overlay is closed, and a manager the taskbar disposes cannot serve them. SystemStatus takes a shared drive manager rather than creating its own, so the taskbar's eject tile and Steam's page are one list rather than two that enumerate every volume twice and disagree. The overlay keeps its format manager behaviour unchanged - it already held one for the controller's lifetime so a format survived the sheet closing - except that the instance now comes from the session, because a format can be started from either surface and both have to see the same run. Two tests cover the half that was missing rather than the code that was already there: the surface is declared with a bridge behind it, and absent without one. Driving it against the live September 2026 beta then found the rest. The format manager enumerates on demand and has no timer, because until now the only thing that opened it was the overlay's format page; Steam's pages have no such moment, so the first live state carried one block device and no drive at all - an orphan pointing at a parent that was not published. The enumeration is now driven off the drive manager's own reconciliation, which already runs on a 2 s signature, plus once at construction. Live state went from 0 drives to 1. The toolkit half of the same finding is in 3bc740a: claiming the transport is not enough, because Steam caches its one availability answer long before the gate can install and never asks again. Verified live: patch Applied then Verified against the running beta, the gate installed, resolved and claimed, and Steam's Speicherplatz page - which does not exist on Windows without it - rendering and navigating with both libraries. Not yet verified: that the drive context menu offers Eject and Format once the cached answers are dropped, and no storage action has been driven from Steam's UI.
…swork Steam detected the card reader and not the library on it. The drive row and the volume row were both published, and nothing tied them together: the volume's parent drive was zero, so Steam saw a reader with no volume on it, which is its definition of "unusable until formatted", and offered exactly that on a card holding a library. The two managers never could have been joined the way this tried. The format manager knows a card by its disk number and the drive manager knows it by its device instance path or mounted media, so comparing their own identifiers matched nothing, ever. A first attempt at matching drive letters was no better: it read a letter the format manager only fills in for cards it has enumerated for erasing, so it was the same guess with more steps. The disk number is the fact both sides are really describing, and reading it is Windows' answer to give. windows-device-control 4d02ecc adds DescribeVolumes for exactly that, and the join is now an integer comparison against what Windows says rather than a string comparison between two components' private handles. That read supplies the rest of what the rows were missing too: the volume's own label and capacity instead of the whole device's, and whether a disk carries any mountable filesystem at all, which is what Steam calls unformatted -- previously hardcoded false, so Steam could neither offer to adopt a blank card nor stop offering it for one already in use. has_steam_library is answered from the volume, since a Steam library is a steamapps directory on it. RemovableDriveEntry keeps the byte count beside its display text. It formatted the size for the taskbar and discarded the number, and Steam wants the number. The projection is logged once per change, and that is deliberate rather than incidental: reading it out of a running Steam means attaching to its renderer, which wedges steamwebhelper when the UI is still building. Every field Steam renders a decision from is in that line, so the published state can be checked from the log with Steam left alone.
The card reader rendered as a drive holding no library, which is what Steam shows as unadopted, and it offered to format a card full of games. has_steam_library tested for a steamapps directory at the volume root. That is only true when the library is the whole drive. A library at D:\SteamLibrary -- the ordinary layout, and the one on the reference machine -- reported false, so Steam concluded the volume carried nothing and treated the drive accordingly. It now reads Steam's own libraryfolders.vdf and compares registered paths by volume root, which is the question Steam is actually asking: not "is this the library" but "does this volume hold one". Unreadable config reports false, which is the safe direction -- Steam offers to adopt a drive that is already a library rather than silently treating an unknown drive as in use. Carries the toolkit gitlink for the adopt-stage fix (d6c4b5c): the stage enum's idle member is 1, not 0, and 0 renders the same permanent spinner an omitted field does. Verified from the projection log on the reference Claw, with Steam left alone: drive 1 'Realtek PCIE CardReader' 256003538944B unformatted=False; volume 2 'SDCard1' D:\ 256002359296B onDrive=1 steamLibrary=True All three of the fields that were wrong are right: the volume is parented to the disk under it, the disk is not reported blank, and the library on it is seen.
The card monitor decided registrations and applied them itself, and it treated "a library is on a mounted volume and Steam does not list it" as sufficient reason to register it. A media-level eject does not remove the card: Windows remounts it within seconds, the volume then looks exactly like a fresh insert, and the next pass put back the registration the user had just ejected. Both surfaces hit it, because both end at the same physical eject. LibraryPolicy now owns every transition -- adopt, eject, format, and what an arriving or departing volume means. CardVolumeMonitor keeps what it is good at, which is detection: volume notifications, the settle delay, the removable and non-system filter, and the re-read that abandons a decision when the media changed under it. What a detection means is no longer its business. An eject is an intent that outlives the remount. It is recorded against the volume root together with the library identity that was on it, and three things clear it: the media actually leaving, a different card appearing in the same slot, or an explicit adopt. Matching on identity rather than on the slot is what makes a swapped card register normally instead of inheriting the intent meant for the card that left, and a volume ejected while carrying no library holds nothing back at all. A refused eject clears it too. The card never went anywhere, and leaving it out of Steam's list would strand a library that is still sitting there -- which is the failure mode this whole change exists to stop, only pointed the other way. Both surfaces reach the policy through RemovableDriveManager.EjectObserver rather than at each call site, so the overlay's panel and Steam's storage page mean the same thing by an eject and the drive manager still knows nothing about Steam libraries. Unregistering runs before the media goes: ejecting first leaves Steam holding a library on a volume that is gone, which its own UI renders as a disconnected drive until something cleans up. Decide moves with the rest of the rule and keeps its tests, renamed to the policy. Five new tests cover the intent itself, including the two cases that would each reintroduce the bug: a swapped card, and a blank volume. 2367 tests pass.
Eject from Steam's storage page did nothing at all -- no request left the client, so nothing reached WSGM to refuse or honour. Steam's library-folder row finds the volume behind a folder by looking for a block device whose mount_paths CONTAIN the folder path, then ejects by the id of whatever that lookup returned: const Vt = V.V(Ar.strFolderPath); // "D:\SteamLibrary" const Fr = async () => V._r.Unmount(Vt.id); Ke(Vr, Ar, Fr, pr ? ni : null); // format gated on Vt.drive_id The volume root was the only mount path published, so the lookup found nothing for D:\SteamLibrary. The eject callback had no id to send and the press was inert, while the overlay's own eject -- which never consults Steam -- worked. Block devices now carry every registered Steam library on the volume alongside the root, which is what the lookup is really asking for. Format appearing in that menu is the proof it worked: it is gated on the drive_id of the same resolved block device. has_steam_library collapses into that read rather than repeating it, and the projection log gains the mount paths and the two support flags. Those flags decide whether Steam draws Eject and Format at all, so a correct row next to an empty menu is now answerable from the log instead of from the client. The eject command logs on arrival and on refusal for the same reason: "nothing happened" has two very different causes and only one of them is WSGM's. Current live state, which is correct on every field: mounts=[D:\ D:\SteamLibrary] | adopt=True unmount=True Eject from Steam's page is still not working with that state published, so the remaining fault is in how the client gates the entry rather than in what WSGM tells it. Format from the same menu does work.
eng\verify.ps1 has been failing on master since 2026-09-10. Its Prettier step rejects four files, and every one of them was last touched by a commit from that day: f1adfbb, 9d630cb and 27bae05. The gate was not run on any of them. Reflow only. No sentence changes, and the diff is symmetric -- 38 lines in, 38 out -- because Prettier is only rewrapping paragraphs to the configured width.
…is gone Two more findings from driving Steam's storage page against the live beta. Steam never sends Format. Its Format Drive modal sends Adopt with the name the user typed, because on SteamOS adopting a drive that carries no filesystem is what erases it. AdoptAsync only ever registered, so "Format" from Steam's page would have registered whatever was already there. It is now two operations under one name, and the disk decides which: nothing mountable means erase and register, behind the same switch as any other erase started from Steam's pages; a filesystem already there means register it. The typed name becomes the label; Steam's validate flag is logged, not acted on -- WSGM's format already re-verifies the disk before every destructive step, and there is no lighter variant of that to offer. The adopt path also carried the identity mismatch fixed earlier for the projection: its mount-path lookup searched the drive manager's list for the format manager's identifier, which never matched, so every adopt of a mounted drive was refused as no longer present. It resolves through the disk number now, like everything else. Steam did not react to a card pulled from the reader or ejected from Windows. ReadState returned null when both managers listed nothing, and null is "nothing to say" by design -- never published -- so the gate kept the last state it had and Steam went on showing a drive that was no longer in the machine. Empty after a scan is a real answer. RemovableDriveManager.HasScanned draws the line between "no removable storage" and "not looked yet", which is the startup race the null rule was protecting against, and only the second stays silent. Records toolkit 3f4a96c: the request-envelope reader that made Steam's eject reach the host at all, and the label and validate flag on adopt. Verified live on the reference Claw: eject from Steam's storage page reaches the host, the policy unregisters the library, and the card ejects. Not yet verified live: the empty-state publication on a physical pull, and the adopt path from the Format Drive modal.
A card pulled from the reader left its library in Steam's list, and the log had no "Card volumes" line at all: the monitor was never running. Two reasons, one behind the other. GameModeCardServicePolicy tied library reconciliation to game mode. That was defensible when the only Steam surface showing cards was a game-mode one; it is not now that Steam's own storage pages are revived whenever the CEF bridge is on, in either mode. The card in the reader has to be the library Steam has whichever mode the shell is in. Reconciliation is gated on the CEF master switch and on not being overlay-test, and on nothing else. Manifest watching stays with game mode: it serves the library tabs. The monitor still waits for the Big Picture window before changing anything, so the wider gate changes when it watches, not when it may act. Widening the policy alone changed nothing, because the desktop-startup path never asked it. ApplyCardServices ran on the desktop-to-game transition, on game-mode boot and on the CEF toggle, but a session that starts next to a live desktop -- every dev-deploy restart, all day -- took the "resuming in desktop mode" branch, which called none of them, and DesktopModeStarting does not fire for a session that never entered game mode. The same class of gap the docs already record for game-mode boot (2026-08-22), on the third entry path. Verified live: "Volume arrival/removal notifications registered" and "Card volumes: reconcile requested (startup)" now follow "resuming in desktop mode" at startup. Not yet verified: the departure purge on a physical pull. Also names the empty projection "no removable storage" in the log; the earlier wording could never print, since the flags were appended before the check.
Pure refactor; behaviour verified live before it and covered by the same 68 tests after it. The volume-root helper existed twice, once in the bridge and once in the library policy, with the same body. It is SteamLibraryVdf.VolumeRoot now, beside NormalizePath, since everything that reasons about "which volume holds this library" keys on it. HasSteamLibrary had no callers left: the projection already reads the registered library paths it needs, and the flag is whether that list is empty. LogProjection took its two support flags from fields that existed only to carry them across one call. They are parameters. Records toolkit 137f7c1, which strips the hunt-time scaffolding from the gate and corrects the surface's description of the client's message vocabulary.
DisposeAsync awaited the owner thread's release with no timeout while that thread waited on a ManualResetEventSlim that was never disposed. The owner's finally always signals today, but the disposing thread holds Global\WSGM.DevicePackageSlot, so any future path that missed the signal would make every other WSGM start refuse with exit code 2. The release signal is now a task that needs no disposal, and the dispose wait is bounded at ten seconds with a warning; process exit still abandons the mutex for the next waiter to recover.
The VIIPER gitlink pins the fork's wsgm branch while the fork's default branch is viiper-controller, and .gitmodules named no branch, so git submodule update --remote followed the wrong branch and a clone that only knew the default branch could not find the pinned revision. .gitmodules now records branch = wsgm and the engineering guide says so. A fresh full clone of the fork reaches the current pin through origin/wsgm.
check-no-live-data-paths.ps1 promised to cover test, fixture, probe and tooling sources but scanned only tests, while verify.ps1 reported the guarantee as met. It now also scans tools, Device Lab, and the device and plugin projects. Widening it found a real case: the UWP launch spike wrote transcripts and staged injection files inside %LOCALAPPDATA%\WSGM; it now uses its own WSGM UwpLaunchSpike root, like Device Lab and the Ally X Lab. The remaining sibling-root resolutions carry the guard's reasoned allow marker.
The SxS assembly identity in app.manifest still said 1.0.0.0 while WSGM.csproj is 2.0.0. Only the release workflow rewrote it, so a hand-built installer shipped a WSGM.exe whose manifest identity disagreed with its file metadata and installer name.
installer/WSGM.iss hardcodes a direct-ISCC fallback version and app.manifest an assembly identity, and nothing compared either with WSGM.csproj, so after a version bump a direct ISCC run named and registered the installer for the old version. eng/check-version-sync.ps1 now fails build.ps1 and verify.ps1 when either copy drifts, and the release workflow stamps the installer fallback from the tag alongside the csproj and manifest so its own verify step agrees.
assert-component-staging.ps1 only scanned the plugin package, and its developer-path pattern required a Source\ segment, so it could not match this checkout's own E:\SourceCode\ root or a JSON-escaped path. The installer copies Tools recursively, yet Tools only got the link check, and it was shipping about 105 MB of native SkiaSharp and HarfBuzzSharp .pdb files that arrive as NuGet runtime assets. Tools now gets the full developer-material and leak scan, App's text files get the leak scan (the rest of App ships through the installer's allowlist), and the path pattern tolerates escaped separators and names the checkout root and the CI workspace shape. Device Lab is published without symbols and its remaining .pdb files are removed. Verified against a real staging tree: the assertion passes, no symbols remain, and a planted escaped repository path is caught.
WSGM.csproj copied Native\Viiper\* into every publish with no SkipNativeArtifacts condition, and the staging directory is only refreshed by eng/build-viiper.ps1, which dev-deploy never ran, so a library left over from an earlier build could be deployed with no warning. build-viiper.ps1 now records the revision it built beside the library, dev-deploy refuses a staged library whose recorded revision differs from the checked-out submodule, and the item honours SkipNativeArtifacts like the Steam Input Lease items. build.ps1 already rebuilds the library on every release build.
The HidHide [Run] step used skipifdoesntexist and Inno ignores [Run] exit codes, so setup finished successfully with physical controller isolation missing, which later showed up only as unexplained duplicate controllers. A [Run] entry cannot see its exit code, so the entry's Check function now runs the installer through Exec, logs a non-zero or failed start and tells an interactive user, and returns False so the entry is not run again; it keeps its place between the USB/IP driver and the restart entries. Compiled with ISCC 6.7.3 against placeholder payloads.
stage-device-components.ps1 and dev-deploy.ps1 checked $LASTEXITCODE after calling another PowerShell script. A script call does not set it, so the guards only repeated the last native command run inside the callee and worked only because the callees throw. They are removed with a note that the callees report failure by throwing; both callers already fail on a missing archive or package afterwards.
DisplayProfiles carried its own DEVMODE layout and mode P/Invokes while the overlay changed modes through WindowsDeviceControl.DisplayModes, giving two independent mode-change paths with no shared gate. It now uses the library's primary-display calls, which share that gate, and NativeDisplay drops the duplicate DEVMODE surface and its now-unused constants. The review's other two pairs stay as they are on purpose: NativeStorage.TryGetDeviceNumber works on an already-open volume handle and also returns the device type, which the volume-identity rules in Shell require, while WindowsStorage.DiskNumberFor opens by drive letter; and each assembly keeps its own internal CreateFile and DeviceIoControl declarations, since the library's are internal.
Process enumeration and each close request, which waits up to eight seconds, now run on a worker thread, so the status text paints instead of the window freezing blank.
The allowance now reads the list again right before writing, and the end of the session removes only the entry the lab added instead of writing back the list it saw before the prompt.
In inverse mode HidHide's list names the applications it denies, so adding the lab hid the controller from it while the session recorded the change as an allowance. The lab now makes no write there and only tells the tester when it is on that list.
The HidHide allowance was the lab's only live write without a durable record, so a killed session left its entry behind silently. The entry is now saved before the write, cleared once removal reads back, and the next start offers to remove a leftover entry.
A Windows.Gaming.Input route was a position in the gamepad list, so a pad disconnecting between discovery and the pulse sent the vibration to a different device. The route now carries the pad's device id and is refused when that pad is gone.
The window title, session record and provenance event each repeated the version, so bumping the project alone mislabelled the evidence. They now read the assembly's informational version.
Path comparison dropped the volume, so an entry for C:\Tools\AllyXLab.exe counted as the same program running from D:\Tools and the consent prompt was skipped. Drive paths are now mapped to their NT device, and a drive that does not map never matches.
A zero write that failed in the pulse's finally block replaced the cancellation or error that ended the pulse. The failure is now logged and the original exception kept; the worker's final zero still reports the cleanup state.
SingleOrDefault throws on a duplicate id, so the prepared refusal after it never ran. The endpoint lookups now count matches and refuse anything but exactly one.
The parent watcher and stdin reader could touch the cancellation source, event and answer queue after the worker disposed them, raising unobserved exceptions on exit. Those objects now live until the worker process ends.
Route discovery opens every HID collection and probes XInput and Windows.Gaming.Input, which made the window hitch when the rumble section started.
The substring test labelled ArmouryCrateControlInterface as Armoury Crate and let table order decide which entry, and so which close rule, a process got.
BUILD.json hashed LF-normalized sources without saying so, and nothing checked it. The new eng/allyxlab-download.ps1 declares that rule, checks the exe against SHA256.txt and BUILD.json, warns when the source has moved on, and rebuilds all three with -Build. eng/verify.ps1 runs the check.
The committed exe predated the review fixes. It is rebuilt from committed source with eng/allyxlab-download.ps1 -Build, which also writes BUILD.json with its declared LF-normalized source hashes. The build ran no Windows UI or Ally X hardware test.
Picks up the attach, handle table, device registry and build revision fixes from the release review.
Picks up the FFI null outcome, recovery scan start and binding connect timeout fixes from the release review.
Qodana Community for C/C++It seems all right 👌 No new problems were found according to the checks applied View the detailed Qodana reportTo be able to view the detailed Qodana report, you can either:
To get - name: 'Qodana Scan'
uses: JetBrains/qodana-action@v2026.2.1
with:
upload-result: trueContact Qodana teamContact us at qodana-support@jetbrains.com
|
Qodana for .NET12839 new problems were found
☁️ View the detailed Qodana report Detected 51 dependenciesThird-party software listThis page lists the third-party software dependencies used in WSGM Contact Qodana teamContact us at qodana-support@jetbrains.com
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The whole 2.0 Changeset.