Skip to content

fix(theme): give AttributeImage a persistent cacheId -- the uncut #120 info-page read storm (#154)#206

Merged
NathanNeurotic merged 1 commit into
masterfrom
claude/attrimage-memo
Jul 16, 2026
Merged

fix(theme): give AttributeImage a persistent cacheId -- the uncut #120 info-page read storm (#154)#206
NathanNeurotic merged 1 commit into
masterfrom
claude/attrimage-memo

Conversation

@NathanNeurotic

@NathanNeurotic NathanNeurotic commented Jul 16, 2026

Copy link
Copy Markdown
Owner

The bisect that cracked it (AndrewBento, HW, #154)

I tested Riptopl with a theme where game screenshots appear on the main screen without having to go to the game information page, and everything loaded and worked normally... I didn't access the game information page... all screenshots loaded every time. The problem isn't with the screenshots or the image loading, but with the game information page itself.

He's right — and the bisect doesn't exonerate the art pipeline, it indicts one caller.

Root cause

cacheGetTexture's negative (FAILED) memo requires both out-params to persist. A failed load writes *cacheId = -2; *UID = gCacheGeneration; and the skip gate is (*cacheId == -2 && *UID == gCacheGeneration).

// themes.c — drawAttributeImage (INFO badges)   ❌ stack local, re-zeroed every frame
int posZ = 0;
cacheGetTexture(..., &posZ, &attributeImage->currentUid, ...);

// themes.c — getGameImageTexture (MAIN covers)  ✅ persistent
cacheGetTexture(..., &item->cache_id[...], &item->cache_uid[...], ...);

So the -2 was thrown away every frame, the skip gate was structurally unreachable, and every frame re-enqueued a fresh failing device open for as long as the info page rendered. AttributeImage was the only art consumer in the tree doing this — which is exactly why Andrew's main-page screenshots cost ~1 open per generation and survived indefinitely.

Info-page-exclusive by layout, not by engine: conf_theme_OPL.cfg puts all seven AttributeImage elements on info7..info13, none on main. It only fires on a disk theme (the cache branch is gated on thmGetGuiValue() != 0; the built-in theme short-circuits to embedded glyphs with zero card reads), and only for badges whose glyph the theme doesn't ship — which the embedded-glyph fallback right above it exists for because that's the normal case.

This is the "BASELINE info-entry read burst that desyncs the card in the first place" that 86da202 admitted it never cut. No #120 attempt ever went near it — all of them sanded ELEM_TYPE_GAME_IMAGE or the VCD cover loader, i.e. the paths that already had a working memo. That's why every beta made it "take longer to happen" and none fixed it.

Fix (4 lines, mirrors the proven GameImage pattern)

mutable_image_t gains currentCacheId, init -1, passed instead of the stack local. A missing glyph now costs one open per generation instead of one per frame.

It also resets in the force-refresh branch — mandatory, not tidiness: the -2 gate short-circuits without comparing the value string, so a leftover -2 from the previous game would suppress the new badge for the rest of the generation.

Scope — honest

This is the trigger, verified line-by-line. The persistence (all art dead afterward) has no code-side explanation — no global art-kill flag exists, request accounting balances. It rests on the fork's own HW conclusion (01aa6d3) that the card desyncs card-side under read load. So this is a prevention fix; an already-desynced card needs a power cycle. HW must confirm before #154/#120 is called closed.

Also refuted (worth recording)

The TerminateThread/RPC-corruption theory is dead on reachability. cacheEnd()'s only caller is gated to Exit / Power Off / Boot Disc, and gDiag.artTerminate (TK) increments inside that same branch — so TK is mathematically 0 for any session you're still browsing in. Every "TK:0" HW reading was vacuous. Info exit issues no abort at all (guiSwitchScreen only calls cacheAdvanceGeneration, whose invalidate runs preserveLoaded=1 and never sets abortRequested).

Validation

Struct changed → validated with a full make clean rebuild (the Makefile doesn't track header deps). Builds clean, exit 0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved texture caching for attribute images to prevent repeated failed texture loads across frames.
    • Cache state now resets correctly when the active configuration changes.
    • Improved rendering consistency by keeping image cache state synchronized with the active image data.

…info-page read storm

AndrewBento (HW, #154) bisected this for us: with a theme that renders screenshots on the MAIN page he
hammered art back and forth indefinitely and never wedged; entering/exiting the PS1 game INFO page kills
artwork for PS2 AND PS1 alike. "The problem isn't the screenshots or the image loading, but the game
information page itself." He is right -- and the bisect does not exonerate the art pipeline, it indicts
ONE CALLER.

ROOT CAUSE: cacheGetTexture's negative (FAILED) memo needs BOTH out-params to persist -- a failed load
writes *cacheId = -2 and *UID = gCacheGeneration, and the skip gate is (*cacheId == -2 && *UID ==
gCacheGeneration). drawAttributeImage passed a STACK LOCAL (`int posZ = 0;`) for the cacheId, re-zeroed
every frame, so the -2 was thrown away, the gate was structurally unreachable, and EVERY FRAME
re-enqueued a fresh FAILING device open for as long as the info page rendered. AttributeImage was the
only art consumer in the tree that did this: every other caller (getGameImageTexture and friends) passes
a persistent field/array -- which is exactly why Andrew's main-page screenshots cost ~1 open per
generation and survived forever, while the info page stormed.

The storm is info-page-exclusive by layout, not by engine: conf_theme_OPL.cfg puts all seven
AttributeImage elements on info7..info13 and none on main. It only fires on a DISK theme (the cache
branch is gated on thmGetGuiValue() != 0; the built-in theme short-circuits to embedded glyphs with zero
card reads), and only for badges whose glyph the theme does not ship -- which the embedded-glyph fallback
right above it exists precisely because it is the NORMAL case.

This IS the "BASELINE info-entry read burst that desyncs the card in the first place" that 86da202
admitted it never cut. No #120 attempt ever went near it -- all of them sanded ELEM_TYPE_GAME_IMAGE or
the VCD cover loader, i.e. the paths that ALREADY had a working memo. That is why every beta made the
wedge "take longer to happen" and none of them fixed it.

FIX (mirrors the proven GameImage pattern): mutable_image_t gains currentCacheId, initialised to -1 and
passed to cacheGetTexture instead of the stack local. A missing glyph now costs exactly ONE open per
generation instead of one per frame. It also resets in the force-refresh branch -- MANDATORY, not
tidiness: the -2 gate short-circuits without comparing the value string, so a -2 left over from the
previous game would suppress the new badge for the rest of the generation.

SCOPE (honest): this is the TRIGGER, verified line-by-line. The PERSISTENCE (all art dead afterwards) has
no code-side explanation -- no global art-kill flag exists and the request accounting balances; it rests
on the fork's own HW-recorded conclusion (01aa6d3) that the card desyncs CARD-SIDE under the read load.
So this is a PREVENTION fix: a card that has already desynced in-session needs a power cycle. HW must
confirm before #154/#120 is called closed.

Also refuted, and worth recording: the TerminateThread/RPC-corruption theory is dead on reachability.
cacheEnd()'s only caller (opl.c) is gated to Exit / Power Off / Boot Disc, and gDiag.artTerminate (TK)
increments inside that same branch -- so TK is MATHEMATICALLY 0 for any session the user is still
browsing in. Every "TK:0" HW reading was vacuous. Info exit issues no abort at all (guiSwitchScreen only
calls cacheAdvanceGeneration, whose invalidate runs with preserveLoaded=1 and never sets abortRequested).

Struct changed -> validated with a full `make clean` rebuild (the Makefile does not track header deps).
Builds clean, exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d8928b6b-8eb2-4763-9578-65e1ff046768

📥 Commits

Reviewing files that changed from the base of the PR and between 87074e8 and 23e5ef5.

📒 Files selected for processing (2)
  • include/themes.h
  • src/themes.c
📜 Recent review details
⏰ Context from checks skipped due to timeout. (15)
  • GitHub Check: build-variants (EXTRA_FEATURES=0, PADEMU=1)
  • GitHub Check: build-lang
  • GitHub Check: build-debug (ingame_ppctty_debug, :v20250725-2)
  • GitHub Check: build-debug (DTL_T10000=1, :v20250725-2)
  • GitHub Check: build-variants (EXTRA_FEATURES=0, PADEMU=1)
  • GitHub Check: build-lang-ps2dev-latest
  • GitHub Check: build-debug (iopcore_ppctty_debug, :v20250725-2)
  • GitHub Check: build-debug-ps2dev-latest (DTL_T10000=1)
  • GitHub Check: build-variants-ps2dev-latest (EXTRA_FEATURES=0, PADEMU=1)
  • GitHub Check: build-debug (iopcore_debug, :v20250725-2)
  • GitHub Check: build-variants-ps2dev-latest (EXTRA_FEATURES=1, PADEMU=1)
  • GitHub Check: build-debug (ingame_debug, :v20250725-2)
  • GitHub Check: build-variants-ps2dev-latest (EXTRA_FEATURES=0, PADEMU=0)
  • GitHub Check: build-variants (EXTRA_FEATURES=0, PADEMU=0)
  • GitHub Check: build-variants (EXTRA_FEATURES=1, PADEMU=0)
🧰 Additional context used
🪛 Cppcheck (2.21.0)
src/themes.c

[warning] 676-676: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)

🔇 Additional comments (2)
include/themes.h (1)

31-39: LGTM!

src/themes.c (1)

676-676: LGTM!

Also applies to: 1203-1209, 1223-1231


📝 Walkthrough

Walkthrough

Adds persistent cache identity to mutable attribute images, initializes and resets it with image state, and uses it during texture lookup to retain cache results across frames.

Changes

Attribute image cache state

Layer / File(s) Summary
Cache state contract and lifecycle
include/themes.h, src/themes.c
mutable_image_t gains currentCacheId; initialization and configuration changes set it to the -1 no-entry sentinel.
Persistent texture lookup
src/themes.c
drawAttributeImage() passes currentCacheId to cacheGetTexture() instead of a stack-local value.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: making AttributeImage use a persistent cacheId to stop repeated failed texture loads.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/attrimage-memo

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

@NathanNeurotic
NathanNeurotic merged commit 84145bb into master Jul 16, 2026
61 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