Skip to content

Identification, cover loading, and Couch Mode fixes on top of 1.7.0 - #37

Merged
btsouth merged 26 commits into
release/1.7.0-mergefrom
release/1.7.0
Sep 7, 2026
Merged

Identification, cover loading, and Couch Mode fixes on top of 1.7.0#37
btsouth merged 26 commits into
release/1.7.0-mergefrom
release/1.7.0

Conversation

@btsouth

@btsouth btsouth commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Stacked on #36. Everything here was written today in response to testing, and several commits are fixes for regressions introduced by earlier commits in this same branch. That history is worth knowing while reviewing: the end state is tested, but it was not arrived at cleanly.

Rendering and loading

  • The combined library rebuilt itself with a full model reset on any signal from any of nine sources, even when the result was identical. Measured over thirty seconds of startup on a 1,464 game library: 26 full resets, 18 of them changing nothing. Now nothing is emitted when unchanged, games added are appended, and a reset is kept for games genuinely moving. A test holds this.
  • Card delegates decoded their artwork twice, once at full size before layout gave them a size. 8,478 image loads became 5,604 over the same window.
  • Covers are decoded once and kept in a bounded 128 MB cache, since Qt keeps roughly twenty. Measured: 136 decoded from disk, 1,071 served from memory, none decoded twice.

Identification

Four separate faults, found in this order:

  1. Four services read credentials from the keyring on separate worker threads at startup. libsecret builds its GObject types on first use and two threads racing corrupts the registration, so every lookup afterwards failed and no credential loaded. Keyring calls are serialised.
  2. The pass waited on the library's change signals, but sources that load from their own database had already filled it, so the wait never ended.
  3. Queuing and processing used different staleness rules. Queuing was version aware; processing judged by timestamp alone, so a game queued because the rules had changed was dequeued, skipped, and never re-identified. This is the one that made the library look frozen while the app was busy.
  4. Prioritising what is on screen only listened for rows appearing and disappearing. Opening a console reports a layout change instead, so walking into a console reordered nothing.

Matching itself: regional platforms (a Super Famicom cartridge is platform 58, not 19, which was 22 of 34 failures), accents, publisher prefixes, and ROM catalogue numbers. A fingerprint test now fails if any matching rule changes without raising kMatchVersion, because I changed the rules once and forgot, leaving 575 entries stuck.

Covers

A portrait is fetched only when a game's own artwork is missing or cannot serve as a cover, decided by shape rather than by system. Steam keeps its official capsules, NES keeps its portrait boxes, N64 and SNES take portraits. Portraits store as JPEG: 161 KB against 726 KB on twelve real files, taking a full SNES set from 983 MB to 218 MB.

Couch Mode and navigation

  • The bar states what is filtered and reaches Source and Sort directly. Ten controls became eight and the widest labels still fit at 720p.
  • Recycled grid delegates stayed focusable while standing for no row, so moving down from the organize row could open a game that was not there. Fixed at the source, with a test that fails without it.
  • Back walks out of a console, then a search, then a source, then everything else.
  • Emulators are asked to launch fullscreen using the flag each one documents. Ryujinx is left alone, since I could not verify one.

Checks

97 CTest cases pass. Six are new and each was verified to fail without its fix. Remote CI has not run on either branch before now. No aarch64 package has been built for this candidate.

Sorting by rating was hard to read, because the score that decided the order
was not on the card. The metadata line now ends with the score beside the
source and the playtime, brighter than the rest of the line so a sorted grid
reads down the column.

Games without a rating show nothing rather than a placeholder, and the source
name gives up the width the score needs so it elides instead of pushing the
line off the card.
Automatic identification already existed, but it accepted a match only when the
cleaned local title equalled the catalogue title exactly, and the cleaner knew
only No-Intro style tags. Measured against a real 1,397 game set, 99% of titles
still carried dump text after cleaning, so almost nothing could ever match and
every game fell to manual identification.

The cleaner now removes a parenthesised group only when every part of it is a
region, language, release flag, revision or translation note, so "(NA, Rev 1)"
and a multi-clause translation credit both go while "(& Knuckles)" and
"(Digital Deluxe Edition)" stay. Square bracket flags go too, and a sorted
article moves back to the front, with or without a subtitle after it. Comparison
ignores a leading article, which the catalogues disagree about. That same set
now cleans completely.

Where several catalogue entries share a name and platform, the most rated one
wins instead of the match being abandoned. A regional duplicate or a
compilation beside the game no longer costs an identification.

Retro consoles keep their own box art: Libretro scans and GameTDB covers are
authentic, so those systems skip SteamGridDB entirely rather than replacing
real artwork with a fan-made portrait. Switch, Wii U and PS4 carry only a
square icon that crops badly, so they still ask for one, as do PC games.
A stored match carried only a timestamp, so an answer decided by older rules
stood until its rating aged out. A matching fix therefore reached an existing
library a month later, one entry at a time, and a shelf marked "Needs
identification" kept looking broken in the meantime. That is why Super Mario
All-Stars stayed unidentified after the cleaner learned to read its dump tag.

Entries now record the version of the rules that decided them, and an entry
decided by older rules is stale however recently it was written. Raising that
version is how a future matching change reaches everyone, with no backfill
command and nothing for anyone to run by hand.

A match the user chose is now protected from automatic re-identification. It
could previously be replaced during an ordinary rating refresh, and picking the
most rated entry among equal titles made that more likely.

Every connection in settings reports its own state through one shared rule.
RetroAchievements and IGDB showed no status at all, so a working
RetroAchievements connection looked like a failed one, and Steam said OPTIONAL
where the others said NOT CONNECTED. Steam and RetroAchievements need both
their fields before they count as connected, and a provider that answered with
an invalid key, a private profile or a rate limit says to check its settings.
A downloaded portrait replaced whatever cover a game already had, including
Steam's official 600x900 capsule. That is a downgrade: fan art in place of the
publisher's own artwork. The rule is now about the artwork rather than the
system. A portrait is fetched only when the game's own art is missing or cannot
serve as a cover, so anything already portrait shaped is left alone, retro
consoles keep their box scans, and Switch, Wii U and PS4 dumps, which carry a
square icon that crops a logo off the card, may still take one.

That decision has to read the source's own artwork, not the resolved cover, or
a portrait already downloaded would justify keeping itself. A role now exposes
what the source provides, before any user choice or download.

The queue paused half a second between games on top of a 350 ms pause before
each IGDB call, and SteamGridDB's calls were not paced at all. Both providers
now pace themselves at the request, so the gap between games only has to yield
to the event loop. Together with the games that no longer make three
SteamGridDB calls each, a library of mostly Steam and retro games finishes in a
fraction of the requests it made before.
A card under Super Nintendo read "RetroArch · Nintendo - SNES / SFC (SNES9X)",
where the heading above it already says Super Nintendo. The core name pushed
the playtime and the rating to the end of the line, and the rating was the
first thing to elide. Inside a console the card now names only its source, so
the fields that differ between games have the room.
Switching a source filter rebuilds the grid, and every cover blinked back to a
placeholder before returning. Two causes, both measured on a real library over
the same thirty second window: 8,478 image loads before this change, 5,604
after.

A delegate is created before layout gives it a size, so binding the image
source straight through started a decode at the cover's full size while the
frame was still zero, then decoded again at the size actually wanted. The
second decode missed the cache, so art that was already on screen had to be
read from disk again. The image now waits for the card to have a size, which
also stops a 600x900 cover being decoded at full size for a thumbnail.

The fade then made the reload obvious. A cover already decoded is ready within
a frame or two, and fading those in is what made a filter change look like the
library was reloading. The fade is kept for art that genuinely has to come from
disk or the network.

Portraits are also cleaned up on their own now. A portrait downloaded over
artwork a game's own source provides is dropped as the library settles, so the
Steam capsules that fan art had replaced come back without anyone running
anything. Steam is judged by source rather than by the file on disk, because
its official capsule downloads on demand and a game whose capsule had not
arrived yet would otherwise still look like it needed fan art.
Nine sources scan at startup and each one signals as it loads, and the combined
library answered every signal with a full reset. A reset destroys and recreates
every card, so covers disappeared and came back several times before the grid
settled. Measured over the first thirty seconds on a 1,464 game library:

  before   26 full resets, 18 of 25 changing nothing at all
  after     0 full resets, 7 appends, 19 rescans that said nothing

The library is now composed first and compared before anything is emitted. A
rescan that finds the same games says nothing, since the view is already right
and a real change to a game arrives through dataChanged. A source that finished
scanning and added games appends them, which keeps every card already on screen
along with its artwork and the selection. A full reset is left for what it is
meant for: games genuinely moving or disappearing.
Ratings and artwork only arrived if you pressed Update and left the window open
long enough. A library of 1,464 games needs about half an hour that way, the
queue lived only in memory, and closing the app threw away the progress and
started again from the top. Games near the end, such as the eight Nintendo 64
titles, never got their turn.

The pass now continues on its own once the library has settled, picks up games
added later, and resumes across launches, since a game that already has a
current answer is skipped. Pressing Stop keeps it stopped until the next launch
or an explicit Update, so it never fights the person using it.

It also identifies what you are looking at first. Opening a console reorders
what is still pending so those games come first, rather than reordering the
work already done. On a real library it identified 38 games in the first minute
with nothing pressed.
Scrolling a few rows and back made every card read and decode its artwork from
disk again, and show a placeholder while it did. Qt keeps only a couple of
megabytes of images no view is currently showing, roughly twenty covers at the
size a card asks for, which is less than a single screenful. So the cache was
guaranteed to miss for any library worth having, and cacheBuffer could not help:
measured against a real library, changing it moved nothing.

Covers now go through Omakade's own provider, which decodes straight to the size
the card wants and keeps the result. Measured over the first forty-five seconds
on a 1,464 game library: 136 covers decoded from disk, 1,071 served from memory,
and not one decoded twice.

The cache is bounded at 128 MB rather than sized to the library. Holding
everything would grow with both the number of games and the cover size setting,
so the largest libraries would be the ones to blow the budget, and it would be
spent on artwork nobody is near. A few hundred covers is several screens in
either direction, which is the distance people scroll and come back from. Past
that a thumbnail decodes again in a few milliseconds, which was never the
problem; the problem was a limit far below one screen.

Also orders identification by system in turn once what is on screen is done, so
eight Nintendo 64 games and two Dreamcast games cannot sit behind 1,387 SNES
ROMs. That is why those two consoles still had no ratings.
Locks in the startup fix so it cannot quietly come back. A source that rescans
and finds the same games must emit nothing, a source that finds more must append
them, and only a game genuinely moving or disappearing may reset. Reverting the
fix makes this fail with five resets where none belong.
The Couch Mode bar spent equal room on Settings and Desktop, which are rarely
touched, while Source and Sort sat inside Browse. Seeing only Steam games with
nothing on screen to say Steam was the only source selected read as a broken
library rather than a filter.

Source and Sort are now bar controls that state their current value, so the
filter is always visible and Emulated is one press away. Source opens Browse on
its source list, which is still the way to every other filter, so the bar loses
a control rather than gaining three. All, Favorites and Recent become one
cycling Show control, and the console toggle carries its state in its highlight
rather than in its label. Ten controls become eight, and the widest possible
labels still fit at 720p.

Grid cards gained the rating as a badge in a fixed corner of the artwork, which
lets the eye read a column of ratings down the grid the way a line of text under
the card cannot at television distance. Cards without a rating have no badge.
Playtime joins the source line, which had room for it.
…of filters

Narrowing the library leaves the grid holding delegates for the games that just
left. Those delegates stay in the scene, stay visible, and carry index -1, so a
move down from the organize row could land on a card nobody can see. Focusing it
set the grid's current row to -1 and opening it asked for row -1, which is the
"Unknown game" page with no title and no source.

A delegate that stands for no row now leaves the focus chain, so navigation
cannot reach it, and it refuses to be focused, activated or favourited. That is
the fix; the rest is depth. The organize row gained the explicit downward target
every other control already had, rather than falling back to picking whatever
was nearest. Activating from either grid checks the row count as well as the
sign, and openGame refuses a row outside the library instead of opening an empty
page. Reverting the delegate change fails the new test.

Back now walks out of the library the way you walked in: out of a console, then
out of a search, then out of a source, and finally out of anything else still
narrowing the view, which goes together so back always reaches the whole library
rather than asking for a press per filter. Levels that are not active are
skipped, so RetroArch inside Nintendo 64 is one press from RetroArch and two
from all sources. Both views are covered, since both watch the same library.
Retro systems were exempt from portraits on the grounds that their box scans are
authentic. Measured against a real library, that only holds for systems whose
boxes were printed portrait:

  NES         4 portrait, 0 the wrong shape
  N64         0 portrait, 9 the wrong shape
  SNES        0 portrait, 111 the wrong shape
  Dreamcast   0 portrait, 2 the wrong shape

So the exemption kept authentic artwork on NES, where it looks right, and kept
wide cartons and square cases everywhere else, where they cannot fill a card
without being cropped or letterboxed. The rule is now the artwork's shape alone.
A box printed portrait is kept whatever system it came from; a box printed wide
or square takes a portrait, which reads better on a card even when it is fan
made. Artwork arriving later is reconsidered, since the startup sweep applies
this same rule and will drop a portrait once authentic artwork turns up.

Portraits are stored as JPEG rather than PNG. Covers are photographs, and
lossless was costing about 750 KB each: a thousand of them would have filled the
artwork cache on its own and then spent its life evicting and re-downloading. On
twelve real portraits from this library JPEG at quality 92 is 4.5 times smaller,
161 KB against 726 KB, which takes a full SNES set from 983 MB to 218 MB and
well inside the default limit. Portraits already stored as PNG keep working and
are still counted by the cache trim.
Just over half the library was matching. Logging what IGDB actually returned for
every failure showed the search was rarely the problem: of 34 failures, only 5
came back empty, while 22 came back with the right game and were then thrown
away by our own platform check.

IGDB catalogues a Japanese release under its own machine. Alcahest, Accele Brid
and Ace wo Nerae are all returned by name and all listed on platform 58, the
Super Famicom, while we accepted only the Super Nintendo's 19. A system now maps
to every platform its games can be listed under, so a shelf of imports is no
longer discarded after being found.

Two smaller causes went with it. Accents are a spelling difference rather than a
different game, so a cartridge labelled Pokemon Stadium 2 now matches the
catalogue's Pokémon Stadium 2. A licensed game is often catalogued with its
publisher in front, so DuckTales matches Disney's DuckTales.

Measured by clearing every failed entry and running the pass again over the same
library: of the eighty games re-decided, sixty-seven matched, where every one of
them had failed before.
Every control in the identify panel is gated on the service being idle, and the
library pass now runs on its own, so the service was almost never idle. Search
IGDB, Not This Game and Choose Portrait sat permanently disabled, and the status
line reported on whatever game the background happened to be working through.

Opening the panel now stands the queue down and holds it, rather than dropping
it, so nothing already worked out is repeated. The status line shows the state
of the game in front of you instead of the background's. Closing the panel puts
the queue back and carries on where it left off.
A game opened from Omakade filled a fraction of the screen and had to be
toggled fullscreen by hand. RetroArch here is configured with
video_fullscreen = "false", so it opened a normal window, Hyprland tiled it
beside Omakade, and nothing asked for anything else. That is not the emulator
misbehaving and it is not the compositor: a launcher should say what it wants.

Each emulator is now asked explicitly, using the flag it actually documents,
checked against the installed binaries rather than assumed:

  RetroArch  --fullscreen
  Cemu       --fullscreen
  PCSX2      -fullscreen
  shadPS4    -f true
  Dolphin    -C Dolphin.Display.Fullscreen=True, having no flag of its own

Ryujinx is left alone. Its launcher prints no options and its fullscreen state
lives in its own configuration, so there is nothing here to pass that I could
verify rather than guess at.
Some ROM sets number their files, so a Game Boy Advance cartridge arrives as
"1636 - Pokemon Fire Red". Searching a catalogue for that finds nothing.

The number is only removed after the title as written has already failed, so a
game that genuinely begins with a number, 1080 Snowboarding or 1942 or 007
Racing, is searched for as written and never mangled. A hand search is never
affected by a retry left over from the background pass.
The regional platforms, accents, publisher prefixes and catalogue numbers all
changed how a match is decided, and none of them raised kMatchVersion. So the
575 entries already marked as needing identification stayed marked, on rules
that no longer applied, and would never have been retried. That is the exact
failure the version exists to prevent, and a comment asking the next person to
remember was not enough.

Every identification rule now folds into one fingerprint that a test pins.
Changing any of them fails that test until kMatchVersion is raised alongside,
so a library cannot be left holding answers the rules would no longer give.
Verified by adding a platform and watching the test fail.
Keys are read from the keyring on a worker thread, so the library can settle
before they get back. The pass looked once, found no connection, and had
nothing to bring it back, so a whole library sat unidentified until something
else happened to change. Measured on a real library: twelve minutes after
launch, none of the 674 entries awaiting the new rules had been looked at.

Credentials arriving now re-arms the pass, for IGDB and for SteamGridDB alike.
Nothing had been identified for hours and the cause was not the matching rules.
Four services keep credentials, Steam, RetroAchievements, IGDB and SteamGridDB,
and each read its key on its own worker thread as the app started. libsecret
builds its GObject types on first use and two threads doing that at the same
moment race, so the registration failed with "cannot register existing type"
and every schema built afterwards was invalid. Ten of those errors landed on
the same millisecond. Every lookup then failed with "the attribute 'service'
was not found in the password schema", no credential loaded, and a pass that
needs a connection quietly did nothing. Keyring calls are now serialised.

Two more faults were hiding behind that one. The pass connected to the
library's signals and waited for the next change, but sources that load from
their own database have already filled it, so those rows arrived before anything
was listening and the wait never ended. It now looks at what is already there.
And a pass that declined because a key was still being read had nothing to bring
it back, so it now tries again shortly, while wanting credentials outright still
waits to be told. A key that fails to load no longer ends the pass either, since
ratings do not depend on it.

Verified on the real library: it now settles, finds the connections, and queues
1,468 games, where before it queued none.
Two places decided it and they disagreed. Queuing asked needsIdentifying, which
knows about the rule version. Processing then judged by the timestamp alone, so
every game queued because the rules had changed was dequeued, sent straight to
the artwork step, and never identified again. Its recorded rule version never
moved, so it queued again next time, forever. The queue drained at four games a
second and wrote nothing, which is why the library looked frozen while the app
was in fact busy.

Both now ask the same question. On the real library, re-identification went from
one game to thirty-one in forty seconds.
Prioritising what is on screen only listened for rows coming and going. Opening
a console swaps the whole view in one pass and reports that as a layout change,
so walking into a console reordered nothing and its games waited behind the
whole library. That is the opposite of what the prioritising was for.

Layout changes now count, and a game whose details are open goes straight to the
front rather than taking its turn.
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: a2827b81-6940-490b-8d9e-821874c4933a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@btsouth btsouth mentioned this pull request Sep 6, 2026
@btsouth
btsouth merged commit b9a6719 into release/1.7.0-merge Sep 7, 2026
4 checks passed
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.

1 participant