Skip to content

2.0 Update - #74

Open
NightHammer1000 wants to merge 805 commits into
1.0from
master
Open

NightHammer1000 wants to merge 805 commits into
1.0from
master

Conversation

@NightHammer1000

Copy link
Copy Markdown
Contributor

The whole 2.0 Changeset.

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.
@github-actions

Copy link
Copy Markdown

Qodana Community for C/C++

It seems all right 👌

No new problems were found according to the checks applied

View the detailed Qodana report

To be able to view the detailed Qodana report, you can either:

To get *.log files or any other Qodana artifacts, run the action with upload-result option set to true,
so that the action will upload the files as the job artifacts:

      - name: 'Qodana Scan'
        uses: JetBrains/qodana-action@v2026.2.1
        with:
          upload-result: true
Contact Qodana team

Contact us at qodana-support@jetbrains.com

@github-actions

Copy link
Copy Markdown

Qodana for .NET

12839 new problems were found

Inspection name Severity Problems
XML highlighting 🔴 Failure 26
Csharp_injection_rule CommandInjection 🔴 Failure 18
XAML Errors 🔴 Failure 2
Namespace does not correspond to file location 🔶 Warning 240
Redundant name qualifier 🔶 Warning 161
Style class not found 🔶 Warning 157
'??' condition is never null according to nullable reference types' annotations 🔶 Warning 113
Auto-property accessor is never used (non-private accessibility) 🔶 Warning 95
Expression is always 'true' or 'false' according to nullable reference types' annotations 🔶 Warning 80
Unused parameter (private accessibility) 🔶 Warning 64
Access to disposed captured variable 🔶 Warning 46
Redundant argument with default value 🔶 Warning 44
Redundant cast 🔶 Warning 39
Type member is never used (private accessibility) 🔶 Warning 37
Non-accessed positional property (non-private accessibility) 🔶 Warning 32
Use null check pattern instead of a type check succeeding on any not-null value 🔶 Warning 27
Conditional access qualifier expression is not null according to nullable reference types' annotations 🔶 Warning 24
Redundant nullable warning suppression expression 🔶 Warning 22
Access to modified captured variable 🔶 Warning 20
Do not use object initializer for 'using' variable 🔶 Warning 20
Assignment is not used 🔶 Warning 17
Parameter is only used for precondition check (private accessibility) 🔶 Warning 12
Variable in local function hides variable from outer scope 🔶 Warning 9
Empty general catch clause 🔶 Warning 7
Usage of <inheritdoc /> is invalid 🔶 Warning 7
Non-accessed positional property (private accessibility) 🔶 Warning 7
Method return value is never used (private accessibility) 🔶 Warning 7
Unused schema declaration 🔶 Warning 7
Unused local variable 🔶 Warning 5
Variable can be declared as non-nullable 🔶 Warning 5
Disposal of a variable already captured by the 'using' statement 🔶 Warning 4
Equality comparison of floating point numbers 🔶 Warning 3
'??' condition is known to be null or not null 🔶 Warning 3
Possible multiple enumeration 🔶 Warning 3
Component of the tuple is never used 🔶 Warning 3
Missing grid column/row setter for non-first child 🔶 Warning 3
Possible overflow in 'unchecked' context 🔶 Warning 2
Formatting is specified, but interpolated string expression is not IFormattable 🔶 Warning 2
Member hides static member from outer class 🔶 Warning 2
Non-accessed field (private accessibility) 🔶 Warning 2
Redundant 'partial' modifier on type declaration 🔶 Warning 2
Possible loss of fraction 🔶 Warning 2
Private field can be converted into local variable 🔶 Warning 2
Redundant type arguments of method 🔶 Warning 2
Redundant using directive 🔶 Warning 2
Path error 🔶 Warning 2
Avoid using 'async' lambda when delegate type returns 'void' 🔶 Warning 1
Collection content is never queried (non-private accessibility) 🔶 Warning 1
Collection content is never queried (private accessibility) 🔶 Warning 1
Expression is always 'true' or always 'false' 🔶 Warning 1
Inconsistent synchronization on field 🔶 Warning 1
Iterator never returns 🔶 Warning 1
Non-accessed local variable is only used to discard the 'out' parameter value 🔶 Warning 1
Parameter hides primary constructor parameter 🔶 Warning 1
Reassigned to plain text 🔶 Warning 1
Redundant catch clause 🔶 Warning 1
Redundant member initializer 🔶 Warning 1
Redundant 'switch' expression arms 🔶 Warning 1
Unsafe context declaration is redundant 🔶 Warning 1
Suspicious type conversion or check 🔶 Warning 1
Auto-property accessor is never used (private accessibility) 🔶 Warning 1
Local function return value is never used 🔶 Warning 1
'with' expression modifies all accessible instance members 🔶 Warning 1
Redundant namespace alias 🔶 Warning 1
Use preferred 'var' style (when type is simple) ◽️ Notice 2812
Use preferred 'var' style (for built-in types) ◽️ Notice 2760
Use preferred style for trailing comma before new line in multiline lists ◽️ Notice 1934
Use preferred style of 'new' expression when created type is not evident ◽️ Notice 806
Invert 'if' statement to reduce nesting ◽️ Notice 356
Use preferred 'var' style (elsewhere) ◽️ Notice 301
Use collection expression syntax ◽️ Notice 252
Prefer using concrete value over 'default' or 'new()' ◽️ Notice 245
Merge null/pattern checks into complex pattern ◽️ Notice 213
Type member is never used (non-private accessibility) ◽️ Notice 154
Member can be made private (non-private accessibility) ◽️ Notice 149
Replace with 'field' keyword ◽️ Notice 145
Remove redundant parentheses ◽️ Notice 98
Method has async overload ◽️ Notice 95
Convert 'if' statement into 'switch' ◽️ Notice 75
Change lock field type to 'System.Threading.Lock' ◽️ Notice 73
RoslynAnalyzers Use concrete types when possible for improved performance ◽️ Notice 66
Loop can be converted into LINQ-expression ◽️ Notice 66
'if-return' statement can be rewritten as 'return' statement ◽️ Notice 50
Literal length can be reduced by using verbatim string ◽️ Notice 49
Put local function after 'return' or 'continue' ◽️ Notice 40
Replace if statement with null-propagating code ◽️ Notice 39
RoslynAnalyzers Mark members as static ◽️ Notice 38
Use object or collection initializer when possible ◽️ Notice 32
Use preferred style of 'new' expression when created type is evident ◽️ Notice 28
Convert property into auto-property with private setter ◽️ Notice 28
Method supports cancellation ◽️ Notice 28
RoslynAnalyzers Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time ◽️ Notice 24
Property can be made init-only (non-private accessibility) ◽️ Notice 18
RoslynAnalyzers CancellationToken parameters must come last ◽️ Notice 16
RoslynAnalyzers Use AsSpan or AsMemory instead of Range-based indexers when appropriate ◽️ Notice 16
Some values of the enum are not processed inside 'switch' statement and are handled via default section ◽️ Notice 16
Merge null/pattern/value checks into 'or'/'and' patterns ◽️ Notice 15
Foreach loop can be converted into LINQ-expression but another 'GetEnumerator' method will be used ◽️ Notice 14
Convert into 'await using' statement or declaration ◽️ Notice 14
Use UTF-8 string literal ◽️ Notice 14
Add/remove qualifier for static members ◽️ Notice 13
RoslynAnalyzers Avoid constant arrays as arguments ◽️ Notice 13
Simplify LINQ expression (use 'All') ◽️ Notice 13
Auto-property can be made get-only (non-private accessibility) ◽️ Notice 12
Inline temporary variable ◽️ Notice 12
Method return value is never used (non-private accessibility) ◽️ Notice 12
RoslynAnalyzers Do not use Enumerable methods on indexable collections ◽️ Notice 11
Convert local variable or field into constant (private accessibility) ◽️ Notice 11
Member can be made static (shared) (private accessibility) ◽️ Notice 11
Convert lambda expression into method group ◽️ Notice 10
Inline 'out' variable declaration ◽️ Notice 10
RoslynAnalyzers Use ArgumentOutOfRangeException throw helper ◽️ Notice 8
Convert property into auto-property (when possible) ◽️ Notice 8
Convert constructor into primary constructor ◽️ Notice 8
Part of foreach loop can be converted into LINQ-expression but another 'GetEnumerator' method will be used ◽️ Notice 8
Merge negated null/pattern checks into complex pattern ◽️ Notice 8
Some values of the enum are not processed inside 'switch' statement ◽️ Notice 8
Possible 'null' value in the path of binding without fallback value ◽️ Notice 8
RoslynAnalyzers Do not ignore method results ◽️ Notice 7
Local variable has too wide declaration scope ◽️ Notice 7
Member can be made private (private accessibility) ◽️ Notice 6
RoslynAnalyzers Convert to 'GeneratedComInterface' ◽️ Notice 6
RoslynAnalyzers Type assertions based on 'assignable from' are confusingly named ◽️ Notice 6
RoslynAnalyzers Forward the 'CancellationToken' parameter to methods ◽️ Notice 5
Replace ternary expression with 'switch' expression ◽️ Notice 5
Member can be made static (shared) (non-private accessibility) ◽️ Notice 5
Use 'with' expression to copy record ◽️ Notice 5
Do not use object initializer for 'using' variable (object initializer expression may throw exception while initializing 'using' variable) ◽️ Notice 5
Avoid using 'async' for methods and functions with the 'void' return type (avoid using 'async' for methods and functions with the 'void' return type and a parameter of type 'System.EventArgs') ◽️ Notice 4
RoslynAnalyzers Use a cached 'SearchValues' instance ◽️ Notice 4
Duplicated sequential 'if' branches ◽️ Notice 4
Merge sequential checks into single conditional access check ◽️ Notice 4
Method has async overload with cancellation support ◽️ Notice 4
Remove redundant pattern-matching parentheses ◽️ Notice 4
Redundant verbatim string prefix ◽️ Notice 4
Replace with primary constructor parameter ◽️ Notice 4
Use alias ◽️ Notice 4
Auto-property can be made get-only (private accessibility) ◽️ Notice 3
Convert into 'using' declaration ◽️ Notice 3
Redundant type declaration body ◽️ Notice 3
Simplify conditional ternary expression ◽️ Notice 3
Use deconstruction ◽️ Notice 3
Use index from end expression ◽️ Notice 3
Use raw string ◽️ Notice 3
RoslynAnalyzers Do not use collection methods for single-item collections ◽️ Notice 3
RoslynAnalyzers Dispose methods should call SuppressFinalize ◽️ Notice 2
RoslynAnalyzers Use char overload ◽️ Notice 2
RoslynAnalyzers Consider using 'string.Contains' instead of 'string.IndexOf' ◽️ Notice 2
Dictionary lookup can be simplified with 'GetValueOrDefault' ◽️ Notice 2
Field can be made readonly (non-private accessibility) ◽️ Notice 2
Method invocation is skipped ◽️ Notice 2
Type check and casts can be merged ◽️ Notice 2
Iteration variable can be declared with a more specific type ◽️ Notice 2
Redundant 'is' ◽️ Notice 2
Redundant range bound ◽️ Notice 2
Redundant string interpolation ◽️ Notice 2
RoslynAnalyzers Convert to 'GeneratedRegexAttribute'. ◽️ Notice 2
Unused parameter (non-private accessibility) ◽️ Notice 2
Append to collection expression ◽️ Notice 1
Arrange null checking pattern ◽️ Notice 1
Add/remove 'this.' qualifier ◽️ Notice 1
Avoid using 'async' for methods and functions with the 'void' return type (avoid using 'async' for methods and functions with the 'void' return type but without a parameter of type 'System.EventArgs') ◽️ Notice 1
RoslynAnalyzers Use ObjectDisposedException throw helper ◽️ Notice 1
RoslynAnalyzers Cache and reuse 'JsonSerializerOptions' instances ◽️ Notice 1
RoslynAnalyzers Do not raise exceptions in finally clauses ◽️ Notice 1
Cast expression can be replaced with lambda return type ◽️ Notice 1
Replace 'switch' statement with 'switch' expression ◽️ Notice 1
Convert into lambda expression ◽️ Notice 1
Convert delegate variable into local function ◽️ Notice 1
Duplicated 'switch' arms ◽️ Notice 1
Event is never subscribed to (non-private accessibility) ◽️ Notice 1
Field can be made readonly (private accessibility) ◽️ Notice 1
Convert static method invocation into extension member call (convert qualified static method invocation into extension member call) ◽️ Notice 1
Merge conditional ?: expression into conditional access ◽️ Notice 1
Parameter output value is always discarded (non-private accessibility) ◽️ Notice 1
Parameter is only used for precondition check (non-private accessibility) ◽️ Notice 1
Property can be made init-only (private accessibility) ◽️ Notice 1
Redundant spread element ◽️ Notice 1
Replace auto-property with computed property ◽️ Notice 1
Tail recursive call can be replaced with loop ◽️ Notice 1
try-catch and try-finally statements can be merged ◽️ Notice 1
Type is never used (non-private accessibility) ◽️ Notice 1
Use 'nameof' expression to reference enum member name ◽️ Notice 1
Convert negated 'is' expression into 'is' expression with negated pattern ◽️ Notice 1

☁️ View the detailed Qodana report

Detected 51 dependencies

Third-party software list

This page lists the third-party software dependencies used in WSGM

Dependency Version Licenses
Avalonia.Angle.Windows.Natives 2.1.27548.20260419 BSD-3-Clause
Avalonia.BuildServices 11.3.2 MIT
Avalonia.Controls.ColorPicker 12.1.1 MIT
Avalonia.Controls.DataGrid 12.0.0 MIT
Avalonia.Desktop 12.1.1 MIT
Avalonia.Fonts.Inter 12.1.1 MIT
Avalonia.FreeDesktop.AtSpi 12.1.1 MIT
Avalonia.FreeDesktop 12.1.1 MIT
Avalonia.HarfBuzz 12.1.1 MIT
Avalonia.Native 12.1.1 MIT
Avalonia.Remote.Protocol 12.1.1 MIT
Avalonia.Skia 12.1.1 MIT
Avalonia.Themes.Fluent 12.1.1 MIT
Avalonia.Win32 12.1.1 MIT
Avalonia.X11 12.1.1 MIT
Avalonia 12.1.1 MIT
FluentAvaloniaUI 3.0.2 MIT
HarfBuzzSharp.NativeAssets.Linux 8.3.1.3 MIT
HarfBuzzSharp.NativeAssets.WebAssembly 8.3.1.3 MIT
HarfBuzzSharp.NativeAssets.Win32 8.3.1.3 MIT
HarfBuzzSharp.NativeAssets.macOS 8.3.1.3 MIT
HarfBuzzSharp 8.3.1.3 MIT
MicroCom.Runtime 0.11.6 MIT
Microsoft.NET.ILLink.Tasks 10.0.12 MIT
SkiaSharp.NativeAssets.Linux 3.119.4 MIT
SkiaSharp.NativeAssets.WebAssembly 3.119.4 MIT
SkiaSharp.NativeAssets.Win32 3.119.4 MIT
SkiaSharp.NativeAssets.macOS 3.119.4 MIT
SkiaSharp 3.119.4 MIT
System.CodeDom 10.0.0 MIT
System.IO.Ports 10.0.0 MIT
System.Management 10.0.0 MIT
Tmds.DBus.Protocol 0.94.1 MIT
ppy.SDL3-CS 2026.722.0 MIT
runtime.android-arm.runtime.native.System.IO.Ports 10.0.0 MIT
runtime.android-arm64.runtime.native.System.IO.Ports 10.0.0 MIT
runtime.android-x64.runtime.native.System.IO.Ports 10.0.0 MIT
runtime.android-x86.runtime.native.System.IO.Ports 10.0.0 MIT
runtime.linux-arm.runtime.native.System.IO.Ports 10.0.0 MIT
runtime.linux-arm64.runtime.native.System.IO.Ports 10.0.0 MIT
runtime.linux-bionic-arm64.runtime.native.System.IO.Ports 10.0.0 MIT
runtime.linux-bionic-x64.runtime.native.System.IO.Ports 10.0.0 MIT
runtime.linux-musl-arm.runtime.native.System.IO.Ports 10.0.0 MIT
runtime.linux-musl-arm64.runtime.native.System.IO.Ports 10.0.0 MIT
runtime.linux-musl-x64.runtime.native.System.IO.Ports 10.0.0 MIT
runtime.linux-x64.runtime.native.System.IO.Ports 10.0.0 MIT
runtime.maccatalyst-arm64.runtime.native.System.IO.Ports 10.0.0 MIT
runtime.maccatalyst-x64.runtime.native.System.IO.Ports 10.0.0 MIT
runtime.native.System.IO.Ports 10.0.0 MIT
runtime.osx-arm64.runtime.native.System.IO.Ports 10.0.0 MIT
runtime.osx-x64.runtime.native.System.IO.Ports 10.0.0 MIT
Contact Qodana team

Contact us at qodana-support@jetbrains.com

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants