Skip to content

fix(ui): bind redraw-tick to full-bleed backdrop, not 1px sentinel#8

Merged
frankieg33 merged 6 commits into
mainfrom
claude/fix-blank-screen-issue-1sv7R
May 15, 2026
Merged

fix(ui): bind redraw-tick to full-bleed backdrop, not 1px sentinel#8
frankieg33 merged 6 commits into
mainfrom
claude/fix-blank-screen-issue-1sv7R

Conversation

@frankieg33

Copy link
Copy Markdown
Owner

Summary

  • Fixes the recurrence of the blank-screen / partial-paint bug seen on v1.0.2 after long idle, where most of the settings window stops being painted and the desktop wallpaper shows through.
  • Root cause: the v1.0.2 force_repaint mechanism bumped redraw-tick on a 1×1 px invisible sentinel Rectangle. Slint's incremental damage tracker only marks elements whose properties actually changed as dirty, so each bump invalidated a 1px tile while the rest of the framebuffer kept showing stale pixels. request_redraw() alone is not enough to dirty Slint's scene; only a property mutation is.
  • Fix: move the redraw-tick binding onto the existing full-bleed backdrop Rectangle (and delete the now-redundant 1px sentinel). Each Rust-side bump now dirties the entire window, forcing a full re-rasterization. Opacity stays in [0.999, 1.0] and alpha-blends FadeTheme.bg onto FadeTheme.bg (the Window background) — no visible flicker.
  • Adds docs/blank-screen-fallback.md describing the escalation order (Win32 RedrawWindow belt → switch to renderer-femtovg → window recreation) if this fix turns out to still be insufficient under longer idle, so future sessions don't re-derive it from scratch.

Why not Win32 InvalidateRect instead

InvalidateRect/RedrawWindow is upstream of Slint's renderer — it convinces Win32 to send a fresh WM_PAINT, but Slint then consults its own internal damage region, sees nothing changed, and re-blits the same stale framebuffer. The bug lives in Slint's damage tracker, so the fix has to mutate a tracked property on a full-window element. Win32 invalidation is documented in the fallback doc as Step 1 of the escalation if this turns out to be insufficient under longer idle.

Test plan

  • cargo check passes (Linux)
  • cargo test passes (62/62)
  • Manual smoke on Windows: open settings window, leave running 30+ minutes, confirm no blank/partial-paint state and no visible flicker from the opacity bump
  • Manual smoke: hide-to-tray and re-show after long idle still produces a clean first frame (the original v1.0.2 scenario must not regress)

https://claude.ai/code/session_01Us9bxTiUworKoSoLs3rLLu


Generated by Claude Code

claude added 2 commits May 15, 2026 01:47
The v1.0.2 force_repaint mechanism bumps redraw-tick on a 1×1 px invisible
Rectangle. Slint's incremental damage tracker only marks elements whose
properties actually changed as dirty, so each bump invalidated a 1px tile
and the software renderer re-blitted the rest of the (stale) framebuffer.
After long idle this surfaced as the blank-screen-with-a-few-elements
report — most of the window kept showing pixels from before, while the
two or three elements that happened to re-evaluate painted correctly.

Move the redraw-tick binding onto the existing full-bleed backdrop
Rectangle (and delete the now-redundant 1px sentinel). Each Rust-side
bump now dirties the entire window, forcing a full re-rasterization.
The opacity range stays in [0.999, 1.0] and blends bg-on-bg with the
Window background, so there is no visible flicker.
Captures the escalation path if the redraw-tick + full-bleed backdrop
fix from 8d82451 is insufficient: Win32 RedrawWindow belt, then switch
to renderer-femtovg, then window recreation as last resort. Also
distinguishes render-staleness symptoms from data-refresh symptoms so
future sessions don't apply renderer fixes to a config-refresh bug.

Copy link
Copy Markdown
Owner Author

@codex review


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

/gemini review


Generated by Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a troubleshooting guide for blank-screen and stale-framebuffer issues and refines the existing redraw mechanism. The previous 1x1 pixel sentinel has been replaced with a full-bleed backdrop whose opacity is bound to the redraw-tick property, ensuring the entire window is re-rasterized by the software renderer. Feedback was provided to correct the documentation's description of matching on platform-specific window handles using the raw-window-handle crate.

Comment thread docs/blank-screen-fallback.md Outdated
In `src/main.rs::force_repaint`, after `request_redraw()`, get the HWND from
the Slint window via `raw_window_handle_06` and call `invalidate_hwnd`. The
Slint API for this is `w.window().window_handle().window_handle()` returning
`Result<WindowHandle, _>`; match on `WindowHandle::Win32(h)` and pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In raw-window-handle 0.6 (the version used by Slint 1.11), WindowHandle is a struct that wraps the RawWindowHandle enum. To match on the platform-specific variant, you should call .as_raw() on the handle returned by Slint's API.

Suggested change
`Result<WindowHandle, _>`; match on `WindowHandle::Win32(h)` and pass
Result<WindowHandle, _>; match on handle.as_raw() { RawWindowHandle::Win32(h) => pass

Per gemini-code-assist review on PR #8: WindowHandle in raw-window-handle
0.6 wraps the RawWindowHandle enum; the variants live on the inner enum
reached via .as_raw(), not on WindowHandle itself.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive fallback playbook for addressing blank-screen or stale-framebuffer issues and refines the existing redraw mechanism. The UI logic was updated to replace a 1x1 pixel sentinel with a full-bleed backdrop whose opacity is bound to the refresh tick, ensuring the entire window is invalidated in Slint's damage tracker. Review feedback correctly identified several technical inaccuracies in the documentation's code snippets, including incorrect Win32 API arguments and invalid Rust pointer casts.

Comment on lines +38 to +43
pub fn invalidate_hwnd(hwnd: windows::Win32::Foundation::HWND) {
use windows::Win32::Graphics::Gdi::{RedrawWindow, RDW_ALLCHILDREN, RDW_ERASE, RDW_INVALIDATE};
unsafe {
let _ = RedrawWindow(Some(hwnd), None, None, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The RedrawWindow call in this snippet contains several errors that will prevent it from compiling with the windows crate (version 0.58 as used in this project):

  1. The first argument hwnd should be passed directly, not wrapped in Some(). RedrawWindow expects a type that implements Into<HWND>.
  2. The third argument hrgnupdate is of type HRGN, which does not implement From<None>. To specify no update region, you should pass HRGN::default() or HRGN(0).
  3. HRGN needs to be added to the use statement for the snippet to be self-contained.
Suggested change
pub fn invalidate_hwnd(hwnd: windows::Win32::Foundation::HWND) {
use windows::Win32::Graphics::Gdi::{RedrawWindow, RDW_ALLCHILDREN, RDW_ERASE, RDW_INVALIDATE};
unsafe {
let _ = RedrawWindow(Some(hwnd), None, None, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN);
}
}
pub fn invalidate_hwnd(hwnd: windows::Win32::Foundation::HWND) {
use windows::Win32::Graphics::Gdi::{RedrawWindow, HRGN, RDW_ALLCHILDREN, RDW_ERASE, RDW_INVALIDATE};
unsafe {
let _ = RedrawWindow(hwnd, None, HRGN::default(), RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN);
}
}

Comment thread docs/blank-screen-fallback.md Outdated
the Slint window via `raw-window-handle` 0.6 and call `invalidate_hwnd`. The
Slint API for this is `w.window().window_handle()` returning a
`Result<WindowHandle, _>`. `WindowHandle` is a wrapper around the
`RawWindowHandle` enum, so match on `handle.as_raw()` and pull out

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In Rust, casting an integer type (isize from h.hwnd.get()) directly to a pointer type via as _ is not permitted. You must explicitly cast to a pointer type first (e.g., as *mut _) before it can be used in the HWND constructor.

Suggested change
`RawWindowHandle` enum, so match on `handle.as_raw()` and pull out
HWND(h.hwnd.get() as *mut _).

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Per gemini-code-assist review on PR #8:
- RedrawWindow takes hwnd directly (Param<HWND>), not Some(hwnd).
- The hrgnupdate parameter is HRGN (Param<HRGN>), not Option<_>; pass
  HRGN::default() and add HRGN to the imports.
- isize -> *mut needs an explicit pointer type (as *mut _, not as _),
  so the HWND construction example uses h.hwnd.get() as *mut _.

Doc-only — no behavior change.

Copy link
Copy Markdown
Owner Author

@codex review


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

/gemini review


Generated by Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a fallback playbook for addressing blank-screen and stale-framebuffer issues and enhances the existing redraw mechanism. The previous 1x1 pixel sentinel in ui/main.slint has been replaced with a full-bleed backdrop whose opacity is bound to a refresh tick, ensuring the entire window is marked dirty in Slint's damage tracker to prevent partial painting. Documentation and constants in src/main.rs were also updated to reflect these changes. A review comment correctly identified an inaccuracy in the documentation regarding the Slint API for obtaining raw window handles and provided a code suggestion for the correct implementation.

Comment thread docs/blank-screen-fallback.md Outdated
Comment on lines +58 to +61
Slint API for this is `w.window().window_handle()` returning a
`Result<WindowHandle, _>`. `WindowHandle` is a wrapper around the
`RawWindowHandle` enum, so match on `handle.as_raw()` and pull out
`RawWindowHandle::Win32(h)`, then pass `HWND(h.hwnd.get() as *mut _)`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The description of the Slint API for obtaining the raw window handle is slightly inaccurate for Slint 1.11. w.window().window_handle() returns a &slint::WindowHandle, which then requires calling .window_handle() (from the HasWindowHandle trait) to get a Result<raw_window_handle::WindowHandle, _>. The as_raw() method is then called on the inner WindowHandle struct.

Suggested change
Slint API for this is `w.window().window_handle()` returning a
`Result<WindowHandle, _>`. `WindowHandle` is a wrapper around the
`RawWindowHandle` enum, so match on `handle.as_raw()` and pull out
`RawWindowHandle::Win32(h)`, then pass `HWND(h.hwnd.get() as *mut _)`.
Slint API for this is w.window().window_handle().window_handle() returning a
Result<raw_window_handle::WindowHandle, _>. That WindowHandle struct has an
as_raw() method returning the RawWindowHandle enum, so match on that and pull out
RawWindowHandle::Win32(h), then pass HWND(h.hwnd.get() as *mut _).

Per gemini-code-assist follow-up review on PR #8: the previous
round dropped one of the two .window_handle() calls. Slint 1.11
requires both — w.window().window_handle() returns
&slint::WindowHandle, and the second .window_handle() (from the
HasWindowHandle trait) returns the
Result<raw_window_handle::WindowHandle, _> we then .as_raw() to
get the RawWindowHandle enum.

Doc-only — no behavior change.

Copy link
Copy Markdown
Owner Author

@codex review


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

/gemini review


Generated by Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive fallback playbook for addressing blank-screen or stale-framebuffer issues and refines the existing UI refresh mechanism. The code changes replace a 1x1 pixel sentinel with a full-bleed backdrop in ui/main.slint, ensuring that periodic property updates force a full-window repaint in the Slint software renderer. A review comment correctly points out that the new backdrop rectangle lacks explicit dimensions, which could cause it to default to a zero size and fail to trigger the intended damage tracking; adding explicit width and height constraints is recommended.

Comment thread ui/main.slint
Comment on lines +317 to +320
Rectangle {
background: FadeTheme.bg;
opacity: 1.0 - root.redraw-tick * 0.000001;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The Rectangle intended to serve as a full-bleed backdrop likely has a size of 0x0 because it is a direct child of the Window (via SettingsWindow) and is not placed within a layout, nor does it have explicit dimensions. In Slint, elements not in a layout default to their preferred size, which for an empty Rectangle is zero. To ensure this element covers the entire window and correctly triggers a full-window repaint in the damage tracker as intended, you should explicitly set its width and height to 100%.

    Rectangle {
        width: 100%;
        height: 100%;
        background: FadeTheme.bg;
        opacity: 1.0 - root.redraw-tick * 0.000001;
    }

Per gemini-code-assist (high-priority) review on PR #8: a bare
Rectangle as a direct child of Window uses its preferred size,
which is 0x0 for an empty Rectangle. The opacity-bound backdrop
introduced in 8d82451 was therefore not actually full-bleed and
its damage region collapsed to nothing — defeating the very fix
it was meant to deliver.

The previous full-bleed-by-comment Rectangle worked only because
the Window itself sets background: FadeTheme.bg and was painting
over the same pixels for free; that masked the size bug as long
as nothing depended on the Rectangle's actual extent.

Set width/height: 100% so the damage tracker dirties the whole
window when redraw-tick mutates.

Copy link
Copy Markdown
Owner Author

@codex review


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

/gemini review


Generated by Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a troubleshooting playbook for blank-screen and stale-framebuffer issues and improves the UI's redraw mechanism. The previous 1x1 pixel sentinel has been replaced with a full-bleed backdrop whose opacity is bound to a refresh tick, ensuring the Slint software renderer re-rasterizes the entire window to prevent stale pixels. Documentation and code comments have been updated to reflect these changes. I have no feedback to provide.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@frankieg33 frankieg33 merged commit 35a40ac into main May 15, 2026
1 check passed
@frankieg33 frankieg33 deleted the claude/fix-blank-screen-issue-1sv7R branch June 4, 2026 13:26
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.

2 participants