Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 40 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

GameModeExecutor is a Rust program for Windows that runs configured
executables when a game starts and stops. Detection is Windows' own verdict —
the lifetime of the Game Bar presence writer process — never a list of games.
the lifetime of the Game Bar presence writer process, or of a process the
person marked as a game in the Game Bar — never a list of our own.
It runs unelevated, connects to nothing, and shows only a notification area
icon.

Expand All @@ -20,11 +21,24 @@ These decide most questions before they are asked.
workaround, and a workaround that fails *visibly and harmlessly* to one that
fails silently. `SetPreferredAppMode` is the one undocumented call in the
program and `docs/design/06-notification-icon.md` says why it was let in.
- **Measure before deciding.** When the documentation leaves the deciding
question open, build the smallest thing that logs what the system actually
does, then decide. Several early designs here were wrong until measured;
the design record keeps the numbers. Do not report a mechanism as working
until a real game session has exercised it.
- **Microsoft's documentation first, then measure.** Before designing on a
Windows behaviour, read what Microsoft documents about it and cite it in
the record; a spike answers only what the documentation leaves open --
how soon a process id is reused, which the pages do not say. When the
deciding question is open, build the smallest thing that logs what the
system actually does, then decide. Several designs here were wrong until
measured, and the design record keeps the numbers. Do not report a
mechanism as working until a real game session has exercised it, and do
not call a reader of real data done until it has read the real data:
a parser of Microsoft's game list passed its own tests and missed the
very title it was written for.
- **Cheap at rest, silent in game.** What players check first is what the
program costs them. The idle look is the only timer the program has and
must stay the cheapest thing it does; during a game the watcher waits on
a handle and does nothing. A change to either is measured on the whole
installed process against the last release -- processor over minutes,
private memory, handles -- with `presence-probe cost` and `footprint`
for the steps, and the figures go in the record and the changelog.
- **Strict and simple over clever.** An unambiguous state ("it is off, fix the
file") beats a fallback whose behaviour needs explaining. Put the strict
option first and argue for a fallback only if it protects something
Expand Down Expand Up @@ -97,7 +111,10 @@ deleted.
OS only through `sensor::Sensor`, and `engine/tests.rs` scripts one to run
whole sessions; a change to the loop gets a scenario there. The updater
reads the network only through `update::feed::Feed`, scripted the same
way.
way. A rule that sits next to a system call is split from it -- the call
passed in as a function, as `sensor::sighting_among` takes the process
path lookup -- so the rule is tested without Windows and the call stays
a line.
- **No test calls an external host**, ignored or not: the script runs the
ignored tests on every developer machine, and a test that needs GitHub
is a test that fails with the Wi-Fi. The network path is measured by hand
Expand Down Expand Up @@ -153,9 +170,9 @@ section, so that two agents write it the same way:
- Once a version is published its section is history: corrected in place
only for an error of fact, never rewritten for taste.

Three tests read this machine's registry — the Known Game List, the Game Bar
registration, the real sensor — which a GitHub-hosted Windows Server runner
does not have. They are `#[ignore]`d with that reason and the script runs
A few tests read this machine — the Known Game List, the games marked by
hand in it, Microsoft's own list file, the Game Bar registration, the real
sensor — which a GitHub-hosted Windows Server runner does not have. They are `#[ignore]`d with that reason and the script runs
them when `CI` is not set. CI must stay green on a stock runner: a test that
needs a real Windows client, a GPU or a game says so with `#[ignore]`.

Expand All @@ -179,8 +196,19 @@ commands; do not.
## Pitfalls that have already cost time

- `sed` and shell substitutions eat backslashes: `GameModeExecutor\FanControl`
becomes `GameModeExecutorFanControl` and `validate` accepts it. Edit files
with a tool that takes literal strings, and grep the result.
becomes `GameModeExecutorFanControl` and `validate` accepts it. So do
string literals in a script that writes a file -- `target\release` became a
carriage return in a Python heredoc. Edit files with a tool that takes
literal strings, and grep the result.
- A shell started by a packaged host -- the Claude desktop application is
one -- may read `%APPDATA%` through the package's private copy: a file
read there need not be the one the watcher reads, and a stale copy of the
maintainer's configuration was quoted as theirs. What the watcher runs
is in its log; ask the maintainer for the file itself.
- Windows' registry change notification never arrives for the Game Bar's
writes to its game list, from an ordinary process, whichever way it is
asked -- eight ways measured, `docs/design/15-marked-games.md`. Do not
build on it again without the reason.
- In PowerShell, `$LASTEXITCODE` is set by native commands only; after a cmdlet
it is stale. Use `try { … -ErrorAction Stop } catch`.
- `Select-Object -First N` **stops the upstream pipeline** once it has N
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,19 @@ a section is written.
icon back, with a notification saying the watcher is watching again —
also when the fix came while the watcher was stopped.

- Games you marked yourself with *Remember this is a game* in the Game Bar
are detected like any other, from their launch — or from the moment you
tick the box, in the middle of a game. Windows never tells the rest of the
system about those, which is why they used to go unnoticed.
- At start, the log says which of the games you marked Microsoft's own list
now knows, so you can untick them and let Windows recognise them itself.
`status` lists the games you marked and says the same.

### Changed

- Looking for a game while none runs costs about a twelfth of what it did:
at rest the watcher now uses 0.026 % of one processor core, against
0.324 % for 0.2.0, measured nine minutes each on the same machine.
- The watcher starts whatever the configuration file says, rather than
exiting with a code and no icon when the file is wrong at logon. `validate`
still reports the exit codes 3 and 4 for scripts.
Expand Down
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ windows = { version = "0.62", features = [
# FindFirstChangeNotificationW: the configuration folder, watched for the
# live reload.
"Win32_Storage_FileSystem",
# RegNotifyChangeKeyValue: Windows' game list, watched rather than polled.
"Win32_System_Registry",
# WNDCLASSEXW names HBRUSH, HICON and HCURSOR, so the window class needs Gdi
# even though this program never draws anything.
"Win32_Graphics_Gdi",
Expand All @@ -59,6 +61,9 @@ windows = { version = "0.62", features = [
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
"Win32_System_Diagnostics_ToolHelp",
# K32EnumProcesses: the process ids alone, which the idle look takes
# every poll instead of a Toolhelp snapshot, about 130 times cheaper.
"Win32_System_ProcessStatus",
"Win32_System_Performance",
"Win32_System_SystemInformation",
"Win32_System_Threading",
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ or anything else with a command line.

**There is no list of games to maintain.** Windows itself decides when a game
is running — the watcher observes the Game Bar presence writer that Windows
starts for one — and it does not poll while you play. It runs as you, with no
starts for one, and follows the games you marked yourself in the Game Bar —
and it does not poll while you play. It runs as you, with no
administrator rights, connects to nothing unless you ask it to look for an
update, and shows nothing but a small icon in the notification area.

Expand Down
10 changes: 6 additions & 4 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
#
# There is no list of games to maintain here. Detection is Windows' own: the
# program watches the Game Bar presence writer, which Windows starts for a game
# and releases when it is gone.
# and releases when it is gone -- and, for a game Windows did not recognise and
# you marked with "Remember this is a game" in the Game Bar, the game itself.
#
# Paths are easiest written between single quotes: TOML takes those literally,
# so Windows separators need no doubling.
Expand Down Expand Up @@ -53,9 +54,10 @@ log_level = "info"
[detection]
# ---------------------------------------------------------------------------

# How often to look for the presence writer while no game is running. This is
# the only polling the program does: once a game starts it parks on the
# writer's process handle and does nothing until Windows releases it.
# How often to look for a game while none is running: the presence writer, or
# a game you marked by hand. This is the only polling the program does, and a
# look costs a few hundredths of a millisecond; once a game starts it parks on
# the process's handle and does nothing until it ends.
poll_interval = "2s"

# After the writer exits, how long to wait for it to come back before running
Expand Down
25 changes: 25 additions & 0 deletions docs/design/00-detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,31 @@ title** — measured from 2.7 s to over two minutes for the same game on the
same afternoon. [Lot 1](01-console-watcher.md) has those figures and the
decision they led to.

## The limit found in the field, 2026-09-20

Every title measured above is in the Known Game List Microsoft distributes,
with an Xbox `TitleId`. A title Windows knows only because the person ticked
*Remember this is a game* in the Game Bar is not: its entry under
`HKCU\System\GameConfigStore\Children` carries `Revision = 1`, no
`TitleId`, no `GameDVR_GameGUID`. For such a title — *Death Stranding 2*,
ticked days earlier, launched at 11:33:57 — Windows recorded the game
(`GameDVR\LastGameActivity` and the entry's `LastAccessed` at 11:34:00,
GameDVR's encoder rebuilt the same second) **and never started the presence
writer**; the maintainer's own Xbox status stayed *Online* rather than
*Playing*. The writer is Windows' verdict for the titles Windows can name to
Xbox, and no verdict at all for the ones the person named. The sentence
above this section that promised "anything Windows treats as a game" held
for every title until that one; [Lot 15](15-marked-games.md) takes the
finding and adds Windows' list itself as a second signal.

Corrected 2026-09-23, with the probe watching: DS2 was in Microsoft's list
all along. Unticked and relaunched, it got a new entry from the distributed
list — `Revision 2691`, a `TitleId` — and the writer started; *Wreckfest 2*
did the same. The person's hand-made entry had been shadowing Microsoft's
own. So the class splits in two: titles ticked before Microsoft listed them,
which unticking brings back, and titles Microsoft does not list at all —
*The Other Side* on this machine — which only Lot 15 reaches.

## Why a user-session program and not a Windows service

- **Session 0 isolation.** A service cannot see the interactive desktop, and
Expand Down
Loading