Skip to content

Native CRT video v2: DDR contract, analog video settings, calibration screen - #225

Merged
wizzomafizzo merged 4 commits into
mainfrom
crt-native-video-v2
Jun 19, 2026
Merged

wizzomafizzo merged 4 commits into
mainfrom
crt-native-video-v2

Conversation

@wizzomafizzo

@wizzomafizzo wizzomafizzo commented Jun 11, 2026

Copy link
Copy Markdown
Member

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 branch fix/native-video-centering and 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)

  • 3 MB region at 0x3A000000; word0 (frame_counter << 2) | active_buffer, word1 magic 0x5A50 / h_offset / v_offset / mode; buffers at +0x1000 / +0x180000.
  • fb0 geometry selects the mode: 352x240 NTSC (0), 720x480i (1), 352x288 PAL (2). Anything else - including an old host's 320x240 - self-disables the writer with a logged warning.
  • word1 is written before the first word0 publish; stop zeroes word0 then word1. Bit layout locked by compile-time static_asserts.

Settings + calibration

  • MiSTer-only Analog video section on the Display page: CRT mode toggle (always shown so HDMI users can switch in), Video standard (NTSC/PAL), Screen position.
  • The toggle is confirm-gated: confirming writes config/zaparoo_launcher_crt.bin and exits with code 42 so Main respawns the frontend under the new mode.
  • Calibration screen: test pattern at true framebuffer pixels (edge frame, action/title-safe outlines, crosshatch); arrows nudge centering live via word1; values persist on exit only (no SD churn from hold-repeat).
  • New Browse.CrtVideo singleton owns standard + offsets, persisted via state.toml with the frontend.toml mirror.

Safe area

  • scene shrinks 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

  • exit-1000 restart previously execvp'd the filtered argv, silently dropping --crt on any restart-applied setting change in CRT mode. Restart now reuses the unfiltered argv.

Deliberately not in this PR

  • 480i is fully plumbed (writer mode 1, 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.
  • No migration of old zaparoo_video_offsets.bin values; offsets start at 0.

Testing

  • just lint, just test (432 tests), just arm32 all green; 16 translation catalogs refreshed.
  • Verified on hardware (MiSTer + CRT): NTSC picture with stride-1408 fb, live offset nudges, exit-42 toggle round trip, writer publishes correctly (control words + pixels confirmed via devmem).
  • Note for testers: Main only loads menu_zaparoo.rbf into 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

  • New Features
    • Added a CRT calibration overlay with H/V offset readout and Save/Cancel.
    • Added native CRT video controls: standard selection (NTSC/PAL), CRT enable/disable, and live H/V offset adjustment with commit/apply.
    • Added MiSTer-only CRT analog controls and writing of the CRT enable configuration.
  • Improvements
    • Persist and clamp CRT settings/offsets, apply standard-based sizing, preserve --crt on restart, and refine CRT safe-area/overscan rendering.
    • Enhanced native CRT forced-path behavior and live offset updates (where supported).
  • Tests
    • Added/expanded CRT settings and normalization/validation coverage.
  • Documentation
    • Updated translations for the new CRT calibration and settings UI.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds native CRT path calibration for MiSTer: new CRT fields in config/persist schemas, a CrtVideoRust cxx-qt model exposing video standard and centering offsets to QML, a v2 DDR native-video writer with dynamic mode selection and runtime offset adjustment, QML settings controls and a full-frame calibration overlay modal, restart argument preservation for --crt, and translation catalog updates across 17 languages.

Changes

Native CRT Path Calibration

Layer / File(s) Summary
CRT config/persist schema and utility functions
rust/zaparoo-core/src/config.rs, rust/zaparoo-core/src/persist.rs
SettingsConfig, SettingsMirror, RawSettings, and SettingsState gain crt_video_standard, crt_h_offset, crt_v_offset fields with serde defaults; new public helpers normalize_crt_video_standard, crt_video_dimensions, crt_mode_id, clamp_crt_offsets, and offset-range constants are added with full unit test coverage.
Settings merge and mirror-to-config for CRT
rust/frontend/src/models/settings.rs
merge_settings clamps CRT offsets from config with snapshot fallback and normalizes crt_video_standard; persist_settings and mirror_settings_to_config are widened to pub(super) and the mirror gains CRT fields.
CrtVideoRust Qt model and QML bridge
rust/frontend/src/models/crt_video.rs, rust/frontend/src/models/mod.rs, rust/frontend/build.rs
New CrtVideoRust/CrtVideo cxx-qt singleton exposes crt_enabled, available_video_standards, current_video_standard, h_offset, v_offset as QML properties and provides set_video_standard, set_offsets, commit_offsets, write_crt_enable_file invokables; MiSTer build calls zaparoo_native_video_set_offsets via unsafe FFI on offset changes.
Rust FFI offset getters and init path
rust/frontend/src/lib.rs
OnceLock statics cache clamped CRT offsets; zaparoo_rust_crt_h_offset/zaparoo_rust_crt_v_offset exported as #[no_mangle] extern "C"; crt_native_path_forced init branch derives dimensions from the config video standard and caches offsets.
MiSTer sysfs framebuffer mode setter
rust/frontend/src/mister_runtime.rs
The run_vmode_with_format call in the CRT branch is replaced by set_fb_mode_sysfs, a new helper that reads /sys/module/MiSTer_fb/parameters/mode, skips writing when unchanged, and logs results.
Native video writer v2 DDR contract
src/app/native_video_writer.h, src/app/native_video_writer.cpp
Replaces fixed 320×240 single-word DDR with v2 two-word (g_word0/g_word1) multi-mode geometry (352×240, 720×480, 352×288); adds packNativeVideoWord1 with static_assert; validates framebuffer at init; initializes from Rust-provided offsets; publishes frames via slot-flip with seq_cst fence; adds setNativeVideoOffsets/zaparoo_native_video_set_offsets with no-op stubs for unsupported builds.
Restart argv preservation for --crt
src/app/main.cpp
ParsedArguments gains originalArgv preserving the unfiltered argument list; execvp on restart uses originalArgv so --crt survives the restart cycle.
CrtCalibrationModal QML component
src/ui/components/CrtCalibrationModal.qml, src/ui/components/CMakeLists.txt
New full-frame calibration overlay renders crosshatch grid, edge frame, and action-safe/title-safe rectangles; handleAction nudges Browse.CrtVideo offsets for directional inputs and commits/closes on accept/cancel; registered in the Zaparoo.Ui QML module.
SettingsScreen CRT controls
src/ui/screens/SettingsScreen.qml
MiSTer-only "Analog video" section adds crtEnabled toggle, crtVideoStandard picker, and crtCalibration action row; wires all three into _cycleFocused, handleAction, fieldCommit, and onClicked via new _requestCrtEnabled, _videoStandardList, _videoStandardDisplay helpers.
Main.qml and MainLayout.qml CRT modal orchestration
src/ui/app/Main.qml, src/ui/app/MainLayout.qml
Main.qml stages CRT toggle/standard changes through confirmPendingRestart (writes enable file, exits 42) and adds openCrtCalibrationModal/closeCrtCalibrationModal lifecycle functions with global input dispatch. MainLayout.qml mounts the crtCalibrationModalLoader, computes safe-area insets, applies full-bleed background margins, and adds crtScrimBackstop edge strips for overscan modal scrim coverage.
Translation catalog updates (17 languages)
src/ui/translations/frontend_*.ts
All 17 language TS files add the CrtCalibrationModal context, update CoreStatusPill indexing/scraping strings to %1/%2 short forms, replace "Settings" with "Settings & Utilities" in HubScreen, and resynchronize Main/MainLayout/SettingsScreen source locations including CRT video standard option strings.

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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • ZaparooProject/zaparoo-frontend#191: Both PRs modify frontend_eu.ts; this PR extends the Basque catalog with the new CrtCalibrationModal context and CRT settings strings that the referenced PR initially established.
  • ZaparooProject/zaparoo-frontend#205: Both PRs extend the same settings persistence infrastructure (settings.rs mirror_settings_to_config/merge_settings, config.rs SettingsConfig/SettingsMirror) and update translation catalogs for UI menu reorganization.

Poem

🐇 Hop! The pixels line up true,
Crosshatch grids of orange hue,
H and V offsets saved with care,
MiSTer CRT, crisp and fair!
Press any button — the rabbit's done,
Analog video, perfectly spun. 📺

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing native CRT video v2 with DDR contract, analog video settings, and calibration screen functionality.
Description check ✅ Passed The description is comprehensive and follows the template with a clear summary, motivation context, detailed testing results, and checklist items completed. All major sections are addressed and the explanation is thorough.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 crt-native-video-v2

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.ts

File contains syntax errors that prevent linting: Line 1: Expected a type but instead found '?'.; Line 1: expected : but instead found version; Line 1: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 1: Expected an expression for the left hand side of the > operator.; Line 2: Expected a type but instead found '!'.; Line 2: expected : but instead found TS; Line 3: expected > but instead found version; Line 2: Invalid assignment to TS> <TS version; Line 3: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 3: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 5: unterminated regex literal; Line 7: expected > but instead found filename; Line 3: Invalid assignment to "en"> <context> <name>AboutScreen</name> <message> <location filename; Line 7: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 7: Expected an ex

... [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 > but instead found filename; Line 1551: Invalid assignment to `"unfinished">



TopStatusStrip
; Line 1557: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 1557: Expected an expression but instead found '>'.; Line 1558: Expected an expression but instead found '%'.; Line 1558: unterminated regex literal; Line 1559: Expected an expression but instead found '%'.; Line 1559: unterminated regex literal; Line 1560: unterminated regex literal; Line 1561: unterminated regex literal; Line 1562: unterminated regex literal

src/ui/translations/frontend_de.ts

File contains syntax errors that prevent linting: Line 1: Expected a type but instead found '?'.; Line 1: expected : but instead found version; Line 1: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 1: Expected an expression for the left hand side of the > operator.; Line 2: Expected a type but instead found '!'.; Line 2: expected : but instead found TS; Line 3: expected > but instead found version; Line 2: Invalid assignment to TS> <TS version; Line 3: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 3: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 5: unterminated regex literal; Line 7: expected > but instead found filename; Line 3: Invalid assignment to "en"> <context> <name>AboutScreen</name> <message> <location filename; Line 7: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 7: Expected an ex

... [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 > but instead found filename; Line 1551: Invalid assignment to `"unfinished">



TopStatusStrip
; Line 1557: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 1557: Expected an expression but instead found '>'.; Line 1558: Expected an expression but instead found '%'.; Line 1558: unterminated regex literal; Line 1559: Expected an expression but instead found '%'.; Line 1559: unterminated regex literal; Line 1560: unterminated regex literal; Line 1561: unterminated regex literal; Line 1562: unterminated regex literal

src/ui/translations/frontend_hi.ts

File contains syntax errors that prevent linting: Line 1: Expected a type but instead found '?'.; Line 1: expected : but instead found version; Line 1: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 1: Expected an expression for the left hand side of the > operator.; Line 2: Expected a type but instead found '!'.; Line 2: expected : but instead found TS; Line 3: expected > but instead found version; Line 2: Invalid assignment to TS> <TS version; Line 3: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 3: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 5: unterminated regex literal; Line 7: expected > but instead found filename; Line 3: Invalid assignment to "en"> <context> <name>AboutScreen</name> <message> <location filename; Line 7: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 7: Expected an ex

... [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 > but instead found filename; Line 1551: Invalid assignment to `"unfinished">



TopStatusStrip
; Line 1557: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 1557: Expected an expression but instead found '>'.; Line 1558: Expected an expression but instead found '%'.; Line 1558: unterminated regex literal; Line 1559: Expected an expression but instead found '%'.; Line 1559: unterminated regex literal; Line 1560: unterminated regex literal; Line 1561: unterminated regex literal; Line 1562: unterminated regex literal

  • 11 others

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

@wizzomafizzo

Copy link
Copy Markdown
Member Author

Attached are the menu and main builds that work with this.

CRT-mode-v2-stuff.zip

ZaparooProject/Menu_MiSTer#4

ZaparooProject/Main_MiSTer#8

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:

  • In CRT mode we adopt a standard size safe area around the edges.
  • The menu core does a "standard broadcast" signal so it goes over the edge for everyone.
  • We add PAL and initial stuff for 480i.
  • All controls are moved out of the OSD and into the Frontend settings.
  • Frontend is responsible for sending settings via the DDR payload.

Looks great for me so far!!! Very nice. It covers my whole screen now and I can adjust it dead centre.

@wizzomafizzo

Copy link
Copy Markdown
Member Author

@asturur

@wizzomafizzo

Copy link
Copy Markdown
Member Author

@theypsilon you might also be interested!

@wizzomafizzo

Copy link
Copy Markdown
Member Author

MiSTer_Zaparoo.zip

Main with fixed PAL mode switch and added CRT mode toggle back to OSD

@wizzomafizzo
wizzomafizzo marked this pull request as ready for review June 18, 2026 00:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between bc1ecf9 and 43b7277.

📒 Files selected for processing (32)
  • rust/frontend/build.rs
  • rust/frontend/src/lib.rs
  • rust/frontend/src/mister_runtime.rs
  • rust/frontend/src/models/crt_video.rs
  • rust/frontend/src/models/mod.rs
  • rust/frontend/src/models/settings.rs
  • rust/zaparoo-core/src/config.rs
  • rust/zaparoo-core/src/persist.rs
  • src/app/main.cpp
  • src/app/native_video_writer.cpp
  • src/app/native_video_writer.h
  • src/ui/app/Main.qml
  • src/ui/app/MainLayout.qml
  • src/ui/components/CMakeLists.txt
  • src/ui/components/CrtCalibrationModal.qml
  • src/ui/screens/SettingsScreen.qml
  • src/ui/translations/frontend_ar.ts
  • src/ui/translations/frontend_de.ts
  • src/ui/translations/frontend_el.ts
  • src/ui/translations/frontend_en.ts
  • src/ui/translations/frontend_es.ts
  • src/ui/translations/frontend_eu.ts
  • src/ui/translations/frontend_he.ts
  • src/ui/translations/frontend_hi.ts
  • src/ui/translations/frontend_it.ts
  • src/ui/translations/frontend_ja.ts
  • src/ui/translations/frontend_ko.ts
  • src/ui/translations/frontend_nl.ts
  • src/ui/translations/frontend_ro.ts
  • src/ui/translations/frontend_sk.ts
  • src/ui/translations/frontend_uk.ts
  • src/ui/translations/frontend_zh_CN.ts

Comment thread src/ui/app/MainLayout.qml Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Fix 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

📥 Commits

Reviewing files that changed from the base of the PR and between 43b7277 and ad63174.

📒 Files selected for processing (17)
  • src/ui/app/MainLayout.qml
  • src/ui/translations/frontend_ar.ts
  • src/ui/translations/frontend_de.ts
  • src/ui/translations/frontend_el.ts
  • src/ui/translations/frontend_en.ts
  • src/ui/translations/frontend_es.ts
  • src/ui/translations/frontend_eu.ts
  • src/ui/translations/frontend_he.ts
  • src/ui/translations/frontend_hi.ts
  • src/ui/translations/frontend_it.ts
  • src/ui/translations/frontend_ja.ts
  • src/ui/translations/frontend_ko.ts
  • src/ui/translations/frontend_nl.ts
  • src/ui/translations/frontend_ro.ts
  • src/ui/translations/frontend_sk.ts
  • src/ui/translations/frontend_uk.ts
  • src/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.
@wizzomafizzo
wizzomafizzo force-pushed the crt-native-video-v2 branch from ad63174 to 3c8adb6 Compare June 19, 2026 08:19
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Keep 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 win

Keep 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

📥 Commits

Reviewing files that changed from the base of the PR and between ad63174 and 4a74864.

📒 Files selected for processing (32)
  • rust/frontend/build.rs
  • rust/frontend/src/lib.rs
  • rust/frontend/src/mister_runtime.rs
  • rust/frontend/src/models/crt_video.rs
  • rust/frontend/src/models/mod.rs
  • rust/frontend/src/models/settings.rs
  • rust/zaparoo-core/src/config.rs
  • rust/zaparoo-core/src/persist.rs
  • src/app/main.cpp
  • src/app/native_video_writer.cpp
  • src/app/native_video_writer.h
  • src/ui/app/Main.qml
  • src/ui/app/MainLayout.qml
  • src/ui/components/CMakeLists.txt
  • src/ui/components/CrtCalibrationModal.qml
  • src/ui/screens/SettingsScreen.qml
  • src/ui/translations/frontend_ar.ts
  • src/ui/translations/frontend_de.ts
  • src/ui/translations/frontend_el.ts
  • src/ui/translations/frontend_en.ts
  • src/ui/translations/frontend_es.ts
  • src/ui/translations/frontend_eu.ts
  • src/ui/translations/frontend_he.ts
  • src/ui/translations/frontend_hi.ts
  • src/ui/translations/frontend_it.ts
  • src/ui/translations/frontend_ja.ts
  • src/ui/translations/frontend_ko.ts
  • src/ui/translations/frontend_nl.ts
  • src/ui/translations/frontend_ro.ts
  • src/ui/translations/frontend_sk.ts
  • src/ui/translations/frontend_uk.ts
  • src/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

@wizzomafizzo
wizzomafizzo merged commit 6fd1d2f into main Jun 19, 2026
6 checks passed
@wizzomafizzo
wizzomafizzo deleted the crt-native-video-v2 branch June 19, 2026 09:57
ZhymonNorman added a commit to Potions-and-Pixels/zaparoo-frontend that referenced this pull request Jun 28, 2026
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).
wizzomafizzo added a commit that referenced this pull request Aug 24, 2026
* 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.
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