Native CRT video v2: DDR contract, analog video settings, calibration screen - #225
Conversation
📝 WalkthroughWalkthroughAdds native CRT path calibration for MiSTer: new CRT fields in config/persist schemas, a ChangesNative CRT Path Calibration
Sequence Diagram(s)sequenceDiagram
participant User
participant SettingsScreen
participant MainQML as Main.qml
participant CrtVideo as Browse.CrtVideo (Rust)
participant NativeWriter as zaparoo_native_video_set_offsets
participant Process as execvp (restart)
rect rgba(70, 130, 180, 0.5)
Note over User, Process: CRT Enable Toggle Flow
User->>SettingsScreen: toggle crtEnabled
SettingsScreen->>MainQML: requestAccept("crtEnable"/"crtDisable")
MainQML->>MainQML: stageCrtToggle(enable) → open restartConfirm modal
User->>MainQML: confirm
MainQML->>CrtVideo: write_crt_enable_file(enabled)
MainQML->>Process: Qt.exit(42) → execvp(originalArgv)
end
rect rgba(60, 179, 113, 0.5)
Note over User, NativeWriter: CRT Calibration Flow
User->>SettingsScreen: activate crtCalibration row
SettingsScreen->>MainQML: requestAccept("crtCalibration")
MainQML->>MainQML: openCrtCalibrationModal()
User->>MainQML: directional input
MainQML->>CrtVideo: set_offsets(h, v)
CrtVideo->>NativeWriter: zaparoo_native_video_set_offsets(h, v) [MiSTer FFI]
User->>MainQML: accept/cancel
CrtVideo->>CrtVideo: commit_offsets() → persist to config
MainQML->>MainQML: closeCrtCalibrationModal()
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Biome (2.5.0)src/ui/translations/frontend_ar.tsFile contains syntax errors that prevent linting: Line 1: Expected a type but instead found '?'.; Line 1: expected ... [truncated 171216 characters] ... ne 1551: unterminated regex literal; Line 1552: unterminated regex literal; Line 1553: unterminated regex literal; Line 1555: unterminated regex literal; Line 1557: expected src/ui/translations/frontend_de.tsFile contains syntax errors that prevent linting: Line 1: Expected a type but instead found '?'.; Line 1: expected ... [truncated 160241 characters] ... ne 1551: unterminated regex literal; Line 1552: unterminated regex literal; Line 1553: unterminated regex literal; Line 1555: unterminated regex literal; Line 1557: expected src/ui/translations/frontend_hi.tsFile contains syntax errors that prevent linting: Line 1: Expected a type but instead found '?'.; Line 1: expected ... [truncated 176507 characters] ... ne 1551: unterminated regex literal; Line 1552: unterminated regex literal; Line 1553: unterminated regex literal; Line 1555: unterminated regex literal; Line 1557: expected
Comment |
|
Attached are the menu and main builds that work with this. And this is the overall plan: https://github.com/ZaparooProject/Menu_MiSTer/blob/b0fe65a4de6ba3d0f5390bf6af1dcb9edb405928/docs/native-video-plan.md I've purposely deferred the 480i stuff since I figured this PR will be open for discussion for a while and I don't want merging to be too hard, but the plumbing should be there. In a nutshell:
Looks great for me so far!!! Very nice. It covers my whole screen now and I can adjust it dead centre. |
|
@theypsilon you might also be interested! |
|
Main with fixed PAL mode switch and added CRT mode toggle back to OSD |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/ui/app/MainLayout.qml`:
- Line 95: The CRT calibration modal property crtCalibrationModalRequested does
not have a corresponding help-bar mapping, causing helpEntries to fall through
to the active-screen branch instead of displaying appropriate help actions for
the modal state. Add a help-bar mapping condition that checks
crtCalibrationModalRequested alongside the modal state checks in the helpEntries
logic (around lines 287 and 1459-1470) to ensure that when the calibration modal
is open, the help bar advertises Settings actions instead of falling back to the
active-screen branch behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8d9c2b35-ae99-4dbb-a267-fbcd693bd855
📒 Files selected for processing (32)
rust/frontend/build.rsrust/frontend/src/lib.rsrust/frontend/src/mister_runtime.rsrust/frontend/src/models/crt_video.rsrust/frontend/src/models/mod.rsrust/frontend/src/models/settings.rsrust/zaparoo-core/src/config.rsrust/zaparoo-core/src/persist.rssrc/app/main.cppsrc/app/native_video_writer.cppsrc/app/native_video_writer.hsrc/ui/app/Main.qmlsrc/ui/app/MainLayout.qmlsrc/ui/components/CMakeLists.txtsrc/ui/components/CrtCalibrationModal.qmlsrc/ui/screens/SettingsScreen.qmlsrc/ui/translations/frontend_ar.tssrc/ui/translations/frontend_de.tssrc/ui/translations/frontend_el.tssrc/ui/translations/frontend_en.tssrc/ui/translations/frontend_es.tssrc/ui/translations/frontend_eu.tssrc/ui/translations/frontend_he.tssrc/ui/translations/frontend_hi.tssrc/ui/translations/frontend_it.tssrc/ui/translations/frontend_ja.tssrc/ui/translations/frontend_ko.tssrc/ui/translations/frontend_nl.tssrc/ui/translations/frontend_ro.tssrc/ui/translations/frontend_sk.tssrc/ui/translations/frontend_uk.tssrc/ui/translations/frontend_zh_CN.ts
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/ui/translations/frontend_eu.ts (1)
203-205:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix brand typo in welcome string translation.
Line 204 uses “Zaparro Frontend-era”, but the product name is “Zaparoo”. Please correct the translated brand spelling to avoid user-facing inconsistency.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/ui/translations/frontend_eu.ts` around lines 203 - 205, The translation value for the source string "Welcome to Zaparoo Frontend" contains a typo where the product name is misspelled as "Zaparro Frontend-era" instead of "Zaparoo Frontend-era". Correct the translation element to use the proper product name "Zaparoo" in the Basque translation to maintain brand consistency across the user interface.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/ui/translations/frontend_eu.ts`:
- Around line 203-205: The translation value for the source string "Welcome to
Zaparoo Frontend" contains a typo where the product name is misspelled as
"Zaparro Frontend-era" instead of "Zaparoo Frontend-era". Correct the
translation element to use the proper product name "Zaparoo" in the Basque
translation to maintain brand consistency across the user interface.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 63e4c3d6-5667-4e81-ba2f-0729b80fd3f9
📒 Files selected for processing (17)
src/ui/app/MainLayout.qmlsrc/ui/translations/frontend_ar.tssrc/ui/translations/frontend_de.tssrc/ui/translations/frontend_el.tssrc/ui/translations/frontend_en.tssrc/ui/translations/frontend_es.tssrc/ui/translations/frontend_eu.tssrc/ui/translations/frontend_he.tssrc/ui/translations/frontend_hi.tssrc/ui/translations/frontend_it.tssrc/ui/translations/frontend_ja.tssrc/ui/translations/frontend_ko.tssrc/ui/translations/frontend_nl.tssrc/ui/translations/frontend_ro.tssrc/ui/translations/frontend_sk.tssrc/ui/translations/frontend_uk.tssrc/ui/translations/frontend_zh_CN.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/ui/app/MainLayout.qml
Adopt the Menu fork's v2 DDR writer contract and move CRT ownership into the launcher now that Main_MiSTer has no OSD video pages: - Rewrite native_video_writer for the v2 control block (3 MB region, word1 magic/mode/offsets, buffers at +0x1000/+0x180000) with fb0 geometry as the mode selector: 352x240 NTSC, 352x288 PAL, and 720x480i supported in the writer but not yet exposed in the UI. - New Browse.CrtVideo singleton: video standard and centering trims persisted through state.toml + frontend.toml, live word1 pokes for calibration, and the zaparoo_launcher_crt.bin enable flag. - MiSTer-only "Analog video" settings section: confirm-gated CRT toggle (writes the enable flag and exits 42 so Main respawns the frontend under the new mode), NTSC/PAL picker, and a screen-position calibration screen with a test pattern addressing true framebuffer pixels. - Central 5% safe-area inset on the scene in CRT mode; backgrounds and screensaver overscan to the true edge. - Fix the exit-1000 restart dropping --crt: execvp now reuses the unfiltered argv. Verified on hardware: NTSC picture, live offset nudges, exit-42 toggle round trip, and writer self-disable against a stale core.
PAL never took effect: the launcher set the framebuffer mode through vmode, whose fb_cmd path goes through Main's /dev/MiSTer_cmd loop and is not serviced while the alt launcher owns video - and Main hardcoded 352x240 anyway, re-asserting it ~1 s after spawn. - Write the fb mode directly via /sys/module/MiSTer_fb/parameters/mode in the CRT path (the mechanism Main itself uses), skipping the write when the geometry already matches. - Extend zaparoo_launcher_crt.bin to [enabled, mode] so Main programs the per-standard geometry on spawn and on its re-assert; new crt_mode_id() maps standards to DDR word1 ids. - Route video-standard changes through exit 42 (Main-owned respawn) on MiSTer instead of the in-process execvp restart; the desktop preview keeps the execvp restart. - Cover the CRT overscan band with Theme.scrim strips while a modal is open: modal scrims fill the safe-area-inset scene, so the full-bleed background previously glowed undimmed around every dialog. Pairs with Main_MiSTer a0c1bae (per-standard fb geometry + OSD CRT toggle). Verified on hardware: PAL 50 Hz picture, NTSC<->PAL round trips, modal scrims reach the framebuffer edge.
The CRT calibration modal had no helpEntries branch, so opening it fell through to the active-screen branch and advertised the wrong actions. Add a crtCalibrationModalVisible branch with Dpad: Adjust and ButtonA: Save, matching the modal's controls (arrows adjust H/V offset, accept/cancel commit and close). Regenerate translation catalogs for the new strings.
ad63174 to
3c8adb6
Compare
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
The crtEnabled field is classified as a toggle control, so keyboard/ gamepad accept enters the toggle block and returns early. That block had no crtEnabled branch, while the handling sat in fieldCommit's deferred path where toggles never reach it. Move the request into the toggle block so accept actually flips CRT mode.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/ui/translations/frontend_ar.ts (1)
33-35:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winKeep the legal notice faithful to the source.
The Arabic text adds “or redistribution,” which changes the license terms beyond the English source. Please remove that extra restriction in both places.
Suggested fix
- <translation>المصدر متاح بموجب ترخيص PolyForm Noncommercial 1.0.0. مجاني للاستخدام الشخصي وغير التجاري. يتطلب الاستخدام التجاري أو إعادة التوزيع ترخيصًا منفصلًا.</translation> + <translation>المصدر متاح بموجب ترخيص PolyForm Noncommercial 1.0.0. مجاني للاستخدام الشخصي وغير التجاري. يتطلب الاستخدام التجاري ترخيصًا منفصلًا.</translation>Also applies to: 213-215
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/ui/translations/frontend_ar.ts` around lines 33 - 35, The Arabic translation in the PolyForm Noncommercial License message in frontend_ar.ts contains an extra phrase "أو إعادة التوزيع" (or redistribution) that does not exist in the English source text. This adds an additional restriction that is not present in the original license terms. Locate the Arabic translation entries for the PolyForm Noncommercial License notice (appearing around lines 33-35 and also at lines 213-215) and remove the phrase "أو إعادة التوزيع" from both occurrences in the translation values to keep them faithful to the English source text.src/ui/translations/frontend_ja.ts (1)
213-214:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winKeep the license notice aligned with the source text.
再配布adds a redistribution restriction that the English source doesn’t mention, so the Japanese notice now states stricter terms than intended.♻️ Proposed fix
- <translation>この無料のソース公開ビルドは個人・非商用利用専用です。商用利用または再配布にはライセンスが必要です。</translation> + <translation>この無料のソース公開ビルドは個人・非商用利用専用です。商用利用には別途ライセンスが必要です。</translation>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/ui/translations/frontend_ja.ts` around lines 213 - 214, The Japanese translation in the license notice at line 213-214 includes "または再配布" (or redistribution) which is not present in the corresponding English source text, creating an inconsistency where the Japanese version states stricter terms. Remove the phrase "または再配布" from the translation so that only the commercial use restriction remains, keeping the Japanese translation aligned with the English source text about what requires a license.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/ui/translations/frontend_ar.ts`:
- Around line 33-35: The Arabic translation in the PolyForm Noncommercial
License message in frontend_ar.ts contains an extra phrase "أو إعادة التوزيع"
(or redistribution) that does not exist in the English source text. This adds an
additional restriction that is not present in the original license terms. Locate
the Arabic translation entries for the PolyForm Noncommercial License notice
(appearing around lines 33-35 and also at lines 213-215) and remove the phrase
"أو إعادة التوزيع" from both occurrences in the translation values to keep them
faithful to the English source text.
In `@src/ui/translations/frontend_ja.ts`:
- Around line 213-214: The Japanese translation in the license notice at line
213-214 includes "または再配布" (or redistribution) which is not present in the
corresponding English source text, creating an inconsistency where the Japanese
version states stricter terms. Remove the phrase "または再配布" from the translation
so that only the commercial use restriction remains, keeping the Japanese
translation aligned with the English source text about what requires a license.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 98af9cc1-3f6d-4996-9211-d3cbf5c10cb1
📒 Files selected for processing (32)
rust/frontend/build.rsrust/frontend/src/lib.rsrust/frontend/src/mister_runtime.rsrust/frontend/src/models/crt_video.rsrust/frontend/src/models/mod.rsrust/frontend/src/models/settings.rsrust/zaparoo-core/src/config.rsrust/zaparoo-core/src/persist.rssrc/app/main.cppsrc/app/native_video_writer.cppsrc/app/native_video_writer.hsrc/ui/app/Main.qmlsrc/ui/app/MainLayout.qmlsrc/ui/components/CMakeLists.txtsrc/ui/components/CrtCalibrationModal.qmlsrc/ui/screens/SettingsScreen.qmlsrc/ui/translations/frontend_ar.tssrc/ui/translations/frontend_de.tssrc/ui/translations/frontend_el.tssrc/ui/translations/frontend_en.tssrc/ui/translations/frontend_es.tssrc/ui/translations/frontend_eu.tssrc/ui/translations/frontend_he.tssrc/ui/translations/frontend_hi.tssrc/ui/translations/frontend_it.tssrc/ui/translations/frontend_ja.tssrc/ui/translations/frontend_ko.tssrc/ui/translations/frontend_nl.tssrc/ui/translations/frontend_ro.tssrc/ui/translations/frontend_sk.tssrc/ui/translations/frontend_uk.tssrc/ui/translations/frontend_zh_CN.ts
💤 Files with no reviewable changes (2)
- src/ui/translations/frontend_zh_CN.ts
- src/ui/translations/frontend_uk.ts
✅ Files skipped from review due to trivial changes (2)
- src/ui/components/CMakeLists.txt
- rust/frontend/build.rs
🚧 Files skipped from review as they are similar to previous changes (14)
- rust/frontend/src/models/mod.rs
- src/ui/components/CrtCalibrationModal.qml
- rust/frontend/src/mister_runtime.rs
- rust/frontend/src/lib.rs
- rust/zaparoo-core/src/persist.rs
- rust/frontend/src/models/settings.rs
- rust/frontend/src/models/crt_video.rs
- src/app/main.cpp
- src/ui/app/MainLayout.qml
- src/app/native_video_writer.h
- src/app/native_video_writer.cpp
- src/ui/screens/SettingsScreen.qml
- src/ui/app/Main.qml
- rust/zaparoo-core/src/config.rs
39 commits since v1.1.1; 7 conflict files, all in the ArtCade-fork surface area: - rust/frontend/src/models/mod.rs (credits/dev_team module decls) - rust/frontend/src/models/settings.rs (5 hide_* qproperties + Initialize wiring) - rust/zaparoo-core/src/config.rs (5 hide_* SettingsConfig fields + RawSettings + settings_config_from_raw helper extension) - src/ui/app/Main.qml (_validStartupScreen → _isStableNavigationScreen helper + ImageOverrides Connections; Credits-family screens added to stable nav) - src/ui/app/MainLayout.qml (help-bar Quit gate + new categoryOptions ButtonX entry) - src/ui/screens/HubScreen.qml (actionEntries redesign: _hubCoverKey, enabled:true, Update tile, requestContextMenu; ArtCade hide_* gates re-applied; Credits tile preserved; Update tile also gated by hide_settings) - tests/ui/tst_navigation.qml (bounds-check the out-of-range cross-row test since action-row size now varies with hide_* + Update + Credits) ArtCade-fork patches verified intact post-merge: - 5 hide_* qproperties + RawSettings + Initialize wiring - HubScreen tile-level hide_* gates (Resume/Favorites/Recents/Settings) - HubScreen Credits tile (always visible — legal-compliance posture) - hide_exit cancel-handler gate + help-bar Quit gate - Credits/Artists/DevTeam/AboutEntries cxx-qt models registered - requestCreditsScreen signal + Main.qml routing Notable upstream features picked up: - ZaparooProject#249 Add hide controls for browse items (show_hidden, hidden_categories, hidden_system_ids) - ZaparooProject#259 launchable systems + Other category surfacing - ZaparooProject#260 hide category index/scrape actions when no indexable systems exist - ZaparooProject#258 user customization for system art, hub icons, names (ImageOverrides model) - ZaparooProject#271 zaparoo-update integration - ZaparooProject#225 Native CRT video v2 settings (crt_video_standard, crt_h_offset, crt_v_offset) - ZaparooProject#251 localized system names + region setting Safety net: tag `artcade-pre-v1.2.1-rebase` pins pre-merge HEAD (1ba850b).
* feat(ui): pin the shine, simplify themes to 3 colors, kill tile pop-in, round the context menu hole Responds to public feedback calling out the frontend's "3d and shiny effect" by identifying the mechanisms behind it, documenting them, and locking them with tests so a refactor can't lose them by accident. - Document the two mechanisms behind "shiny": PressableSurface's bottom/front specular edge, and the tinted-logo tritone gradient map that reads as an emboss (docs/style.md). - Collapse ColorSchemes presets from 22 hand-authored hex values to 3 authored colors (primary/accent/text) with every other role derived via explicit channel lerps, direction-aware so it works for both dark and light presets. Adds a light preset (paper-blue). Removes the one-off stateMarker role in favor of Theme.accent for the favorite heart and Hidden badge. - Kill pop-in on bundled artwork (Systems logos, Hub icons, Settings tile icons) by baking SVGs to a mapped, uncompressed atlas (resources/baked/icons.zbin) read via mmap instead of parsed at request time, and rewriting TintedSvgImageProvider as a plain synchronous QQuickImageProvider so a cache hit resolves in the same frame instead of forcing a thread-pool round trip. Color-mode PNG logos are explicitly kept asynchronous. Includes a standalone tools/bake-icons rasterizer, ordering/gating fixes so nothing waits on a blank frame, and a tint_lut unit extracted for byte-exact parity testing. - Unify BrowseList's selected-row light direction with PressableSurface: a recessed channel shades oppositely to a raised tile, so the dark keyline moves from the top edge to the bottom edge. - Cut the ContextMenu scrim's hole to the anchored tile's actual rounded-rect shape instead of a rectangle, using baked corner masks (Qt Quick-rendered complement alpha, r = 1..16) so no bright square notches poke past the tile's corners. Adds tst_tint_lut, tst_baked_icon_atlas, tst_corner_masks, and QML coverage (tst_color_schemes, tst_pressable_surface, tst_press_cues, tst_tile_cover_sync, tst_resources, tst_paged_grid, tst_context_menu, tst_sizing). Full lint and test gates pass. * feat(ui): round 2 polish — recessed settings rows, accent-tinted artwork, theme-responsive icons, rasterize-at-painted-size, 240p bitmap type Ten fixes from a 540p pass over the ui-overhaul redesign: - Settings rows adopt the BrowseList recessed-slot latch instead of a pressable surface (LatchSurface.qml extracted so both share it) - Tinted artwork's flat-source fallback moves from the highlight slot to the midtone, and every logo ramp is rebuilt on luma-ordered poles so the light preset's gradient no longer inverts - Status icons, nav arrows and the loading spinner route through the tinted-svg provider so they aren't invisible on the light theme - Header/About/screensaver logo art now shares one Resources.logoUrl() rung+variant lookup, backed by a re-exported 12-file PNG ladder (monolithic logo.png/logo-crt.png deleted) - Type ladder, tile caption inset, and grid gaps nudged for legibility at 540p/480p - Context menu labels center as a measured Text item with content-driven panel width instead of a wide fixed floor - The global loading cue no longer jumps vertically when a screen's own overlay takes over, and TATE no longer paints it unrotated - Hub's source caption hides during a forward transition instead of lingering under "Loading…" - Favorite-heart outline gets a dedicated markerOutline color and a thicker keyline so it's visible in every theme - Every Image sourced from a provider or SVG now pins sourceSize so it rasterizes once at its painted size instead of decoding small and upscaling (nav glyphs, screensaver bounce, About screen) - Bitmap type (MxPlus font, no-antialias, 8/16px quantization) now auto-engages on embedded hardware at 240p even without --crt, split out from crtNativePath so CRT layout (overscan, grid density, high-DPI pin) stays a separate concern — --crt behavior is unchanged by construction since bitmapType always equals crtNativePath there just lint, just test, and just verify-baked all pass. * feat(ui): round 4/5 — palette semantic tier, OKLCh ladder, 24-preset catalog Round 4: eight feedback items from a review pass over the round 2/3 redesign, rooted in one cause — the palette pipeline ran primitive (3 hexes) straight to ~21 component roles with no semantic tier in between, so `accent` carried five unrelated jobs. - Insert a semantic tier (onAccent, onAccentMuted, marker) between the three authored colors and the component roles, and rebuild every role that carries the accent's own hue (logo ramps, onAccent, onAccentMuted, marker) in OKLCh instead of per-channel sRGB lerp, which doesn't preserve chroma. Fixes near-white focus artwork and an orange tint reading brown. Add _clampAccent as a seed-agnostic guardrail so an arbitrary future custom accent can't ship illegible. - tint_lut.cpp's single-tone fill switches from the highlight rung to the midtone, so flat single-color system logos paint in the accent itself when focused instead of a near-white rim-light color. - BrowseList's inline tag suffix and Tile's caption both gained the variantColor binding they were missing, fixing illegible tags on a selected row. - New Theme.marker role (fixed hue, rotated away from the accent) replaces Theme.accent for the favorite heart and Hidden badge so they no longer blend into the focus ring. - SettingsField's toggle track now carries on/off state at max contrast against the row's own current background in both row registers, replacing the old rule where track and knob branched on row-selection independently of each other. - PressableSurface's focus ring now reuses Tile's two-stacked-filled- rounded-rect construction instead of a thin border, which steps visibly at corners under the software renderer (QTBUG-123210). - Modal and ListPickerModal size to measured content instead of a fixed percentage of the viewport, mirroring ContextMenu's existing pattern. Fixes a width-clamp bug where the default floor could override a caller-supplied smaller panelMaxWidth. - Fixed a live regression in SettingsScreen._fieldControl (landed in 6fd1d2f/#225) that made 17 settings rows render blank with no chevron; picker rows now get a chevron too, action rows get an accent-tinted label instead. Round 5: a bigger sandbox of real color schemes to live-test the round-4 ladder against, plus a couple of fixes surfaced along the way. - Catalog grows from 3 to 24 presets: real, unmodified hex triads from Catppuccin, Nord, Dracula, Gruvbox, Tokyo Night, Rosé Pine, Kanagawa, Ayu, Nightfox, Monokai, One Dark Pro, Everforest Dark, Synthwave '84, plus retro/console references (amber/green phosphor, Neo Geo, NES, Virtual Boy). Two guardrail floors relaxed to admit them without touching a hex (text/card AAA 7:1 -> AA 4.5:1; focus-ramp primary- rung chroma floor 45% -> 33%). SettingsScreen's display-name if-chain becomes a lookup table; rust settings.rs's COLOR_SCHEMES extends to match. - Color-scheme picker rows show a 3-color swatch preview (the preset's own authored primary/accent/text) instead of just a name. ListPickerModal renders it only for entries carrying a swatch array, so every other picker stays pixel-for-pixel unchanged. - Toggle knob no longer disappears into a selected row's accent fill: found a constant neutral fill can't clear 3:1 against every track state across 24 presets, so the fix is a border in the track's own "on" color instead — reusing contrast guarantees the existing guardrail tests already enforce. - Help bar text nudged down slightly (Sizing.pctH(0.4)). just lint and just test pass (qmllint, clang-tidy, cargo nextest 652/652, ctest 411/411 QML/C++ UI tests, lupdate-refreshed translations). ctest's baked_icon_atlas failure is pre-existing, unrelated host/pinned-Qt-image SVG rasterization drift, untouched by this work. * feat(ui): round 5 — status line replaces the core pill, footer chrome, hub layout persistence Kills CoreStatusPill's stadium chip and spinner in favor of StatusLine, a borderless full-row message ladder (connection > active task > terminal message > transient event) paired with ProgressTrack, a segmented block bar whose leading cell hard-blinks instead of throbbing. Surfaces Core's per-system indexing/scraping detail that was previously deserialized and dropped, adds a transient status-event bus (status_events.rs) for token scans / playtime warnings / inbox messages, and teaches mock-core to push scripted notification sequences so the whole ladder is reachable in run-dev. Adds a footer row (PageIndicator, EmptySlot, count badge) to the browse grids, replacing the right-side scrollbar/gutter, and persists hub layout state through new hub_layout modules in both Rust crates. Extracts QrMatrix out of QrCodeModal for reuse. Also fixes a real rendering bug found while investigating an apparently-unrelated test failure: tint_lut.cpp's flatPremul table was built from the highlight color instead of the midtone, so every single-tone system logo served from the baked icon atlas rendered washed-out near-white instead of its accent color. * feat(ui): round 6 — Add to Hub shortcuts, empty-slot skip navigation, page cue relocation - Add "Add to Hub" context-menu action on Systems/Games (system, folder, and zapscript shortcuts) via a new HubLayout::add_target_item, plumbed through the cxx-qt bridge as a qinvokable. - PagedGrid gains skipEmptyCells: cursor paths (moveSelection, pageBy, mouse hover/click) treat blank cells as unreachable, with an Android-FocusFinder-style nearest-tile search for vertical moves on freely arranged boards. The Hub arms this outside a Move session; empty slots no longer paint a focus ring at all. - Move the grid page/count cue (count badge + PageIndicator chevrons) up onto TopStatusStrip's title line for every theme except CRT, which keeps the footer placement since its top strip is hidden entirely. - Rework PressableSurface's focus ring to derive thickness from cardBorderWidth instead of a screen-relative percentage, and dim/brighten its label on focus the same way Tile already does. - Regenerate all translation catalogs via lupdate. * feat(ui): round 7 — tile state consolidation, inverse-video menus, 19-preset catalog - Fold hidden/disabled tile states into the caption's dim tag suffix and a muted front edge (Tile.qml, TileLoader.qml), replacing the opacity-dimmed TileBadge overlay (removed) with a non-opacity cue. - Hub tiles (Resume, Update, categories) now always render with a disabled/stateReason pair instead of appearing or disappearing on live Core/internet state, fixing a layout-shift bug and a lost-focus-on-boot bug in restoreFromCategoriesReset. - ContextMenu and ListPickerModal rows switch from PressableSurface press cues to inverse-video SelectionBar selection, matching SettingsField and BrowseList rows. - Grow the color scheme catalog from 11 to 19 presets (Gruvbox, Everforest, Solarized Dark/Light, Rosé Pine, Oxocarbon, Flexoki Paper, Game Boy) and retune Classic Purple's background/accent pairing. - Drop Motion.pressMs from 80ms to 34ms so DeferredAction's Accept hold costs at most one MiSTer frame instead of a fixed animation tax. - Add Settings & Utilities and Quit to the Hub's View menu; Quit is no longer bound to Cancel/B on the Hub root. - Size Settings' category grid against the Hub's own resolved tile size (Sizing.hubTileSize) instead of an independent cap, and add a System row to the detail pane for mixed-system Recents/Favorites list views. - Regenerate Qt translation catalogs for the above (365 source strings). * feat(ui): round 8 — settings legibility, hub art, back-routing, cover cache - Settings screen: replace the per-row description line with a shared two-line hint band pinned under a static card frame, and fix the scroll/snap math so the top and bottom rows land flush instead of cutting off card margins. - Step selected-row text to Font.Medium (SelectionBar.contentWeight) to correct irradiation thinning on inverse-video rows; no-op under bitmap mode. - Action rows (SettingsField control: "action") center their label instead of relying on accent tint alone, matching iOS's own convention; their live status readout moves to a second centered line. - Cache Hub-linked game covers and the Resume tile's cover for first-frame availability: a bounded (<=22 entry) manifest of Core thumbnail paths at cache/frontend/hub_covers.toml (hub_cover_manifest.rs), seeded before Qt starts on MiSTer, refreshed on hub layout / resume changes. Also fixes a latent bug where resume cover refresh checked the wrong model field and could never fire for a Hub-only session. - Re-detect MiSTer output resolution while the frontend is already running (mister_runtime::watch_for_output_change), self-restarting silently on a confirmed change so a TV that was off at boot is no longer stuck at the no-EDID 720p guess for the rest of the session. - Persist a `GamesState.entered_from_hub` breadcrumb so Back from a Hub-entered system/folder returns to the Hub and survives a game launch's process kill/relaunch, instead of landing on Systems. - Tighten Tile padding (compactPadding) on the Hub grid and Settings' category grid so full-bleed icon/cover art reads larger. - Remove the "Token scanned" header status message. - Regenerate Qt translation catalogs for the above. Also two bugs found testing round 8 on real hardware: - Restoring a saved deep-page position (e.g. after exiting a game deep in a large folder) blocked the Qt event loop for ~25s: PagedGrid's Repeater builds one QML object per model row regardless of visibility, and the bulk-restore path inserted its entire fetched chunk (up to 1000 rows) in one synchronous call. Route bulk fetches through the same frame-gapped sub-batch path ordinary pagination already uses (games.rs), and retention-gate PagedGrid's per-row skeleton/hit-area behind their own Loaders so off-screen rows only cost the bare delegate item. - MiSTer's kill/relaunch startup restore could resolve the wrong system for the Games screen: `_restoreSystemsScreenSelection` looked up the saved system within `SystemsModel`'s currently-loaded category, which tracks the Hub's own last-viewed category rather than the saved game's category. On a mismatch it silently fell back to browsing whatever system happened to be first in the wrong category, which Core then rejected outright ("unknown system"). Browse the saved system_id directly whenever one is known, regardless of whether it was found in the possibly-wrong category's listing. * feat(ui): rounds 9-10 — on-device fixes, scrape setup modal, per-screen layouts Round 9 (from testing round 8 on real MiSTer hardware): - Resume hub tile prefixes with "Resume: <game>" when a game is ready to resume. - Tile focus ring: derive compact padding from the ring geometry so the ring can never touch tile art again (round-8 regression). - Fix inverse-video row truncation on selection: ContextMenu/ ListPickerModal row labels now measure via a declarative TextMetrics bound to the live selected weight instead of a stale FontMetrics snapshot (round-8 regression). - Fix Settings scroll chevrons rendering at (0,0): re-anchor to settingsCard, a sibling, instead of the no-longer-adjacent flickable (round-8 regression). - Remove the "Discover arcade alternate versions" setting; the feature stays on unconditionally (further restricted to the Arcade system in round 10, see below). - Locale-grouped thousands separators for indexed/scraped counts (new Format.qml singleton). - Fix Settings hint band collapsing to one line: reserve height from a real FontMetrics.lineSpacing measurement instead of a magic multiplier (round-8 regression). - Settings remembers sub-page position within a visit, resets on exit to Hub. - Tighten About page's top/bottom margins. - Replace em dashes with colons across every user-visible string. - Fix Hub tiles missing their bottom edge for ~1s on boot: animate a press-progress scalar instead of raw geometry, so a layout pass can't be mistaken for a press. - Page/scroll chevrons dim instead of vanish when a direction has nothing to scroll to. - Fix rapid-scroll snapshot rendering scaled down instead of 1:1. - Fix page cue/chevrons not appearing until the first input on a freshly-entered grid. Round 10 (from testing round 9 on real MiSTer hardware, plus a batch of design/settings work): - Fix pressed-tile bottom edge showing square corners mid-animation: wrap the front-edge Rectangle in a fixed-height clipping Item instead of resizing the radiused rectangle itself, which Qt was reclamping toward square as it shrank. - Fix cover art flashing the "no cover" chip during a paused bulk/jump fetch: separate "confirmed absent" from "haven't asked yet" in games.rs/favorites.rs/recents.rs's cover-key resolution. - Reorder the 19 theme presets into family blocks (Zaparoo identity, retro/console, editor/terminal, light) instead of addition-history order. - Restrict "Discover alt. versions" to the literal Arcade system — the backend only supports MiSTer's MRA/_alternatives scheme, not Core's broader 32-system Arcade category. - Game details dialog: fix the background grid's rapid-scroll ghost snapshot popping in/out while scrolling the dialog (a held-repeat input path was arming rapid-nav tracking on the screen behind a modal regardless of which modal owned input); fix the Zaparoo logo painting over the dialog (every modal Loader lacked an explicit z above HeaderBar's); redesign internal chrome with section-header dividers, a card-framed cover band, and a fixed tag-label column. - Rename "Write with phone" to "Write with App" — the feature is specifically the Zaparoo App's QR flow. - Split the single browsing-layout setting into independent Systems and Games preferences, with a one-time migration from the old combined value. - Add missing About page contributors. - Move Maintenance to the top of the Library settings page; give every remaining settings row help text in the hint band. - Add a scraper setup modal (scraper choice + re-scrape toggle + Start), replacing a hardcoded "gamelist.xml" scraper id; move the re-scrape toggle out of Settings and into it. - Page/scroll chevrons now hide entirely when there's only one page/screenful, refining round 9's dim-not-hide rule. Regenerate Qt translation catalogs for all of the above. * feat(ui): round 11 - settings copy pass, media job pickers, detail-list fixes From testing round 10 on real MiSTer hardware, plus a full copy pass and follow-up fixes from a second on-device round: - Resume hub tile falls back to the play glyph instead of staying blank for a game with no cached cover: resume_cover_key_for now enqueues its fetch unconditionally instead of respecting the grids' request pause. - Hub footer label's side inset now tracks the page indicator's measured width instead of a fixed third of the screen, roughly doubling its truncation budget; SystemsScreen/MediaListScreen footers get the same treatment for their left-corner count text. - Rewrite all 24 settings row descriptions for clarity and the two-line budget, iterated one at a time against on-device feedback. Add a CRT-tier (bitmap-quantized fonts, ~316x216 safe canvas) truncation regression test alongside the existing desktop-tier one. - debugLogging now stages through the same restart-confirm modal as crtEnabled/language/resolution instead of silently persisting until next launch. - Add a shared system-scope picker (all systems / all systems in a category / one system) to both media jobs: ScrapeSetupModal gains a Systems row, and Update media database gets its own IndexSetupModal (previously ran immediately with no scope). - Detail list view: cover now blanks-then-fades like the grid instead of showing an hourglass; fix the Rating row's descender clipping against the tag table's bottom edge; wire up the page cue in list layout (previously never mounted) and fix Left/Right paging by the list's own visible row count instead of the grid's column x row math. - Add root_distinguishers so a system with multiple root paths shows which physical root each row came from (e.g. "fat" vs "usb0"), reusing the disambiguatingTags role folders otherwise leave blank. - Surface folder/root item counts as a dim suffix in both grid and list layout, as a bare locale-formatted number rather than a "N item(s)" word phrase -- that slot's Text.ElideLeft reliably truncated the word form down to a bare "...tem(s)" on real hardware. Fix the same unfilled-numerus-form bug on the adjacent favorites-with-count string while touching this code. - Fix a pre-existing (round 7) delegate-binding bug: PagedGrid's cellItem.disabled was a plain (non-required) property, so Qt never wired it to the model's disabled role and the Hub's front-edge muting cue for Resume/Update/unconfirmed-category tiles silently never rendered. Every Browse model that feeds PagedGrid now publishes the role explicitly, matching the entryType/fileCount precedent. Regenerate Qt translation catalogs for all of the above. * fix: CI truncation failure and CodeRabbit review findings on PR 392 CI fix: - SettingsScreen.qml: shorten the runScraper description so it fits two lines at the CRT tier; the round-11 rewrite made it long enough to elide, failing both settings-truncation tests. CodeRabbit findings addressed: - mister_runtime.rs: watch_for_output_change() returned immediately when no boot baseline exists (always true on MiSTer --crt, since resolve_video_size skips BOOT_RENDER_SIZE on that path). The caller read that as "output changed" and queued a restart on every CRT launch. Now stays pending forever on that path, matching the desktop no-op. - favorite_systems.rs / FavoriteSystemsScreen.qml: media_count_for_system is a bare qinvokable with no notifying signal, so a background media count refresh never re-evaluated the Favorites screen's tags binding while parked on a row. Added a media_counts_revision qproperty and read it from the QML binding as an explicit dependency, same pattern as HubLayout.revision. - Main.qml: a Hub "system" shortcut to a launch-only (virtual) system bypassed the launch-only guard SystemsScreen's own accept handler uses, routing into an empty games browse instead of launching. Now applies the same is_launchable_system guard before navigating. - baked_icon_format.h / baked_icon_atlas.cpp / tinted_svg_image_provider.cpp / tools/bake-icons/main.cpp: mask planes were packed tight at `width` bytes/row and handed to QImage's raw-buffer constructor as bytesPerLine, violating Qt's documented 4-byte scanline alignment contract for any non-4-aligned width. Corner masks (baked at radii 1-16) hit this on most radii. Rows now pad to a shared paddedStride() helper on both the bake and read side; format version bumped to 2 and the atlas re-baked. - AboutScreen.qml: replaced anchors.horizontalCenter + Text.AlignHCenter (forbidden by CLAUDE.md's integer-pixel rule) with Sizing.center()'d items rendered left-aligned; the two multi-line credit blocks became a Column of individually centered Text items so each line still reads centered rather than left-justified as a block. - HeaderBar.qml: extracted the duplicated 600/135 logo aspect ratio into one named property shared by both consumers. Not changed (reviewed, not applicable): the ContextMenu.qml >8-entry clipping note is an existing, documented content-style backstop, not a runtime bug; the docs/architecture.md persisted-metadata note documents an already-deliberate, already-approved exception (see HubLayout::add_target_item's own doc comment). Verified: full ninja build, ctest (8/8), cargo nextest (740/740), and `just lint` (rust + cpp + qml + translations + baked-icon manifest) all green. * fix: ContextMenu scrolls to keep the focused row reachable past 8 entries Follow-up to fca4e9b's review of CodeRabbit's ContextMenu.qml finding. That finding was wrongly dismissed as inert: buildContextMenuEntries's static owner branches do cap at 7, but the "Discover alt. versions" submenu (Arcade only) swaps in one entry per result from Browse.AlternateVersions, capped at MAX_ALT_RESULTS = 64 in alternate_versions.rs. Real arcade sets routinely have well over 8 region/revision variants, so panel's clip: true was silently hiding rows that move() could still navigate to and accept. ContextMenu.qml: the row Column now lives inside a non-interactive Flickable (rowViewport) that slides contentY to keep currentIndex's row inside the visible band, mirroring ListPickerModal.qml's existing viewport/rowColumn/_scrollCurrentIntoView() construction. Scroll position resets to 0 on open and whenever entries are swapped on an already-open menu. Below the 3-8 entry cap this is a no-op -- contentY stays pinned at 0 and rendering is unchanged. tests/ui/tst_context_menu.qml: three new regression tests covering the 30-entry scroll-into-view path, the sub-8 no-scroll path, and scroll reset on an in-place entries swap. Verified: ctest (8/8, including the 3 new ContextMenu cases) and `just lint` both green. * fix(hub): resume focus arm no longer clobbers a real restored item Regression report: the Resume tile stole focus on every startup, not just a genuine first boot with nothing saved. Root cause: Main.qml's _maybeArmHubResumeFocus runs HubScreen's focusResumeIfVisible() right after restoreFromCategoriesReset on every boot (both the immediate Component.onCompleted path and the delayed startup-restore path). restoreFromCategoriesReset is the real authority on Hub focus -- it seats a persisted category/item when one exists and falls back to Resume, deliberately without persisting, only when there is nothing else to restore. focusResumeIfVisible ignored all of that: it re-selected and committed Resume unconditionally whenever resumeActionVisible was true, which defaults true until Core positively confirms there is nothing to resume, so it fired on essentially every launch. MiSTer relaunches the frontend around every game launch, so this clobbered a real saved category/item with Resume on every single relaunch, not just first boot. HubScreen.qml: focusResumeIfVisible now only commits when restoreFromCategoriesReset already landed on Resume itself (its own fallback path); a real restored item is left untouched. tests/ui/tst_persistence.qml: new regression test exercising the full restore -> arm sequence, confirmed to fail against the prior behavior (reverted the fix locally and reran to verify) before restoring the fix. Verified: ctest (8/8) and `just lint` both green.
Summary
Adopts the Menu_MiSTer fork's v2 native video contract and moves CRT ownership into the launcher, now that Main_MiSTer has dropped its OSD video pages and
status[9]CRT bit. Pairs with the Menu fork branchfix/native-video-centeringand the corresponding Main_MiSTer changes - all three must be deployed together for the CRT path (HDMI is unaffected; mismatched combos fail obviously, by design).Writer (DDR contract v2)
0x3A000000; word0(frame_counter << 2) | active_buffer, word1magic 0x5A50 / h_offset / v_offset / mode; buffers at+0x1000/+0x180000.static_asserts.Settings + calibration
config/zaparoo_launcher_crt.binand exits with code 42 so Main respawns the frontend under the new mode.Browse.CrtVideosingleton owns standard + offsets, persisted via state.toml with the frontend.toml mirror.Safe area
sceneshrinks 5% per side in CRT mode only; Sizing bindings propagate it everywhere with no per-screen changes. Background and screensaver overscan to the true edge.Fixes
--crton any restart-applied setting change in CRT mode. Restart now reuses the unfiltered argv.Deliberately not in this PR
crt_video_standard = "480i"accepted in frontend.toml for smoke tests) but not offered in the picker - the UI flicker-discipline pass for interlace comes separately.zaparoo_video_offsets.binvalues; offsets start at 0.Testing
just lint,just test(432 tests),just arm32all green; 16 translation catalogs refreshed.menu_zaparoo.rbfinto the FPGA at its own startup - reboot after copying a new core or you'll see the noise pattern.This PR stays in draft while testers run it; it will be rebased onto main periodically.
Summary by CodeRabbit
Release Notes
--crton restart, and refine CRT safe-area/overscan rendering.