microsoft / microsoft/microsoft-ui-xaml

ListView/ItemsStackPanel stops clipping rows at the bottom viewport edge once content extent reaches ~2²¹ DIPs

Open
#11,225 5 comments 0 reactions 0 assignees View on GitHub
bug needs-triage
Dominant language
C++
Stars
8.4k
Forks
942
Avg merge
2d 7h
Merged PRs (30d)
105

Description

### Describe the bug

In a vertically-scrolling `ListView` (`ItemsStackPanel`, fixed-height rows), once the **total content extent** (`ScrollViewer.ExtentHeight`) grows large, the renderer **stops clipping rows at the bottom edge of the viewport**. The last partially-visible row paints **in full** past the `ListView`'s bottom bound, plus a sliver of the next (below-viewport) row, into whatever is laid out below (in our case a transparent status band over a Mica `SystemBackdrop`, so the strays are clearly visible).

The failure is **gated by the total content extent** — not by scroll position, item template, or selection. Two decisive facts:

1. A list of **70,000 rows leaks at the bottom edge even at `VerticalOffset = 0`** (scrolled to the very top), while **60,000 rows never leak at any offset**. Same viewport, same tiny coordinates at the bottom edge — the only difference is the total `ScrollViewer.ExtentHeight`.
2. The breaking point is the **same item-count range at both 100% and 200% display scale**.

So the limit is a fixed extent of **~2²¹ DIPs (2,097,152)** in *logical* coordinates, independent of physical pixels — which points at a reduced-precision (e.g. 32-bit float) coordinate in the clip/composition path, not a GPU-surface / pixel-buffer size limit (that would move with scale).

### Why is this important?

Virtualized `ListView` exists **specifically** to display large collections — yet this bug corrupts rendering exactly when the collection gets large, defeating the control's core purpose. The threshold is low and easily reached in normal use, and it is scale-independent so no display configuration
avoids it. Notably, one 2²¹-DIP extent limit explains every independent report: ~63,550 rows at 33 DIP (this repro), ~52,452 at ~40 DIP jpeterstewart/ItemsView_Issue), ~30,000 at ~70 DIP (#1876).

- **It hits real, shipping scenarios — not a contrived stress test.** Found in a hosts-file editor:
community ad-blocking hosts files routinely run 100,000–250,000+ entries. Any data-heavy WinUI 3 app
— log/telemetry viewers, database and file browsers, packet/network tools, message and history lists
— can cross the threshold in ordinary use.
- **The failure is severe and obviously "broken."** Whole rows paint *outside* the control, on top of unrelated UI (status bars, adjacent panes, the window backdrop). It reads as visual corruption, not a subtle seam — the kind of glitch that fails Store review and erodes trust in the app and the platform.
- **App developers cannot detect or work around it.** Layout and UI Automation report everything as correct, so bounds/clip logic can't catch it, and every standard mitigation fails (`UIElement.Clip`, `ScrollViewer.CanContentRenderOutsideBounds`, `ItemsStackPanel.CacheLength`, removing ransitions). The only workaround is to cover the area with an **opaque** element — impossible for the transparent / Mica / Acrylic layouts that Fluent design actively encourages (and which reveal the bug in the first place).
- **It is long-standing and independently reproduced.** The same overflow was reported in 2020 (#1876, closed *not planned*) and again in a standalone repro (jpeterstewart/ItemsView_Issue) — it has quietly affected developers for years.
- **Likely a small, contained fix.** The sharp onset at exactly 2²¹ DIP, scale-independent, points at a single reduced-precision coordinate in the clip/composition path — high user-visible impact for what is probably a low-risk change.

### Steps to reproduce the bug

A complete, ready-to-run project is attached (`ClipEscapeRepro`, unpackaged WinUI 3, `dotnet run`). The essence:

- A `Grid` with rows `Auto,*,32`. A `ListView` bound to *N* trivial fixed-height (33 DIP) string items fills the `*` row; a **transparent** `Border`/`Grid` sits in the 32px row; the window uses a **Mica** `SystemBackdrop` so the band below the list reveals the backdrop.
- Load **N = 70,000**, on Windows 11 at **either 100% or 200%** display scale. **Observe:** the bottom row paints in full below the list's edge into the status band — visible **even at the top of the list (`offset = 0`)**, no scrolling required.
- Load **N = 60,000**. **Observe:** the bottom edge clips cleanly at every scroll position. (60k↔70k is the boundary; larger counts such as 100k/200k break more obviously.)

[ClipEscapeRepro.zip](https://github.com/user-attachments/files/29712701/ClipEscapeRepro.zip)

### Actual behavior

Once the total content extent exceeds the threshold:

- The last partially-visible row paints **in full** below the `ListView`'s bottom edge.
- The first below-viewport row paints **partially** below that.
- These pixels land on top of whatever is laid out below the list, because they composite **beneath** later-painted siblings but **outside** the list's clip.
- Layout is unaffected — UIA bounding rectangles are all correct/clamped.

### Expected behavior

Every row is clipped to the `ListView`'s (viewport's) bottom edge regardless of how many items the list contains. A row only partially inside the viewport paints only for the portion inside; nothing paints below the control's bottom bound. This is the behavior at small extents and it should not
change as the list grows.

### Screenshots

Status bar correct at bottom with 60K rows:
Image

Status bar broken at bottom with 70K rows:
Image

From the attached repro, **`offset = 0`** (scrolled to the very top of the list):

- **`70K_broken.png`** — 70,000 items (extent 2,310,000 DIP, just over 2²¹). The row past the last full row paints **below the list's bottom edge**, down into the transparent status band over Mica.
- **`60K_clean.png`** — 60,000 items (extent 1,980,000 DIP, just under 2²¹). The bottom edge clips cleanly; nothing paints into the band.

The same 60k↔70k boundary reproduces at both 100% and 200% scale.

## Threshold data (attached minimal repro: 33-DIP rows; boundary identical at 100% and 200% scale)

| Items (N) | Content extent (DIP) | Result |
|------------:|---------------------:|:---------|
| 60,000 | 1,980,000 | Clean |
| **~63,550** | **2,097,152 (2²¹)** | boundary |
| 70,000 | 2,310,000 | Broken |

The breaking extent is pinned to **2²¹ DIP (2,097,152)** — clean at 1,980,000 DIP (60k), broken at 2,310,000 DIP (70k). **It is a DIP-space (logical-coordinate) limit:** the same 60k↔70k range breaks at both 100% and 200% scale, so the threshold is fixed in DIPs, not in physical pixels (which would
move with scale). The bug was first seen in a production app whose rows are taller (~40 DIP), where it began around ~50,000 rows — the same *extent*, confirming an extent/coordinate threshold rather than an item-count one.

### NuGet package version

2.2.0

### Windows version

Windows 11 (24H2): Build 26100

### Additional context

## What we've ruled out

**Not a layout bug.** UIA `BoundingRectangle` for the `ListView` and for every realized `ListViewItem`
stays correct and clamped to the control bounds the whole time the stray pixels are on screen. The
overflow is purely in the rasterized/composited output.

Each of these was tried and had **no effect** on the overflow:

- Explicit `UIElement.Clip` (a `RectangleGeometry`) on an ancestor of the `ListView`.
- `ScrollViewer.CanContentRenderOutsideBounds="False"` on the list's scroll viewer.
- `ItemsStackPanel.CacheLength="0"`.
- Removing `ItemContainerTransitions`.

And these were **confirmed irrelevant** by bisection (the bug reproduces identically with or without
each) — useful for narrowing where the defect is *not*:

- **Item template.** Reproduces with a flat single-`TextBlock` row, a row with `CornerRadius`, and a
rich row (a `CheckBox` + three editable `TextBox`es, matching the production template) — all identical.
- **`SelectionMode`** — `Single` vs `Extended`: no difference.
- **`ItemContainerTransitions`** — present vs absent: no difference.
- **Scroll offset.** Broken at `offset = 0` (top of the list) just as much as when scrolled down; the
gate is total extent, not the current offset. A 50k list is clean at every offset; a 100k list
leaks even before you scroll.

## Additional observations

- **Extent-gated.** Larger `ExtentHeight` → breaks, at the same (even zero) scroll offset. Core signal.
- **Directional.** Only the **bottom** edge leaks; the **top** stays correctly clipped.
- **Live, not a stale frame.** Scrolling away and back re-creates the artifact every time.
- **Disappears at the true end.** Scrolled to the absolute bottom the artifact is gone — no rows exist
below the viewport to leak.
- **Paint order respected.** The strays composite *beneath* siblings painted later in the tree, so an
opaque element below the list occludes them (our workaround) — consistent with a clip/viewport
escape rather than a z-order bug.

## Suspected root cause (hypothesis)

*Speculative — offered to narrow the search, not asserted.* Two measured facts constrain it:

- **DIP-space (scale-independent).** The boundary is the same extent at 100% and 200%, so the failing
value is a **logical** coordinate in the layout → DirectComposition mapping, not a GPU pixel/texture
dimension.
- **Onset at ~2²¹ DIP.** 2²¹ = 2,097,152 is where a **32-bit float**'s step over that coordinate
reaches 0.25 DIP (ULP = 2^(e−23); in [2²¹, 2²²) → 2⁻² = 0.25). The escape begins exactly as
`ExtentHeight` enters that binade — clean at 1,980,000 DIP (in the 0.125-DIP binade), broken at
2,310,000 DIP.

A 0.25-DIP rounding cannot by itself move a clip by a whole 33-DIP row, so the likely mechanism is that
the **bottom content-clip is *disabled*, not mis-placed**, once precision degrades — e.g. the bottom
clip plane is derived from the large extent/bottom coordinate and a rounding-sensitive step (roughly
`min(viewportBottom, contentBottom)`) collapses, so the clip no longer bounds the visible bottom edge.
Two observations fit:

- **Only the bottom leaks; the top stays clipped.** The top clip is anchored near 0 (small, exact); the
bottom is derived from the ~2²¹ coordinate (imprecise), so the bottom is the one that corrupts.
- **The escape is bounded by realized/cached rows** — with `CacheLength=0` just the partially-clipped
row, with the default cache that row plus the next — i.e. the clip is off/no-op and whatever is
realized past the edge simply shows, rather than the clip being shifted by a fixed amount.

**Where to look:** the InsetClip / content clip applied by the `ScrollViewer` content presenter (and/or
`ItemsStackPanel`'s realization/viewport clip) — specifically where the **bottom** boundary is computed
from `ExtentHeight` / the scroll-content transform and written to a Composition property. A 32-bit
float (or otherwise reduced-precision) carrier there would produce exactly this DIP-space, ~2²¹ onset.

## Workaround

Because the stray rows composite **beneath** content painted later in the visual tree, giving the
element below the list an **opaque** background occludes them. Cosmetic only — the rows are still
painted out of bounds underneath it.

## Not yet tested / open questions

- **Panel type** — reproduced with `ItemsStackPanel`; not tested with `ItemsWrapGrid` /
`ItemsRepeater` + `StackLayout`.
- **Hardware/OS coverage** — observed on one machine (Windows 11 build 10.0.26200); unknown how
broadly it reproduces across GPUs / driver versions.

*(Resolved above: the breaking extent is pinned to 2²¹ DIP, and the limit is DIP-space — the same
boundary holds at 100% and 200% scale.)*

## Related reports / prior art

Searched microsoft/microsoft-ui-xaml and microsoft/WindowsAppSDK before filing — **no open issue
tracks this**. Closest existing material:

- **[jpeterstewart/ItemsView_Issue](https://github.com/jpeterstewart/ItemsView_Issue)** (a standalone
repro repo, not filed in a Microsoft tracker) independently reports the ListView "overflowing its
lower boundary after item **52,452**, regardless of item size." Same bug — and consistent with the
**2²¹ DIP extent** limit: at that repro's ~40-DIP rows, 52,452 × 40 ≈ 2,098,080 ≈ 2²¹. The gate is
the **extent (~2²¹ DIP)**, not the item count (so "regardless of item size" holds only at a *fixed*
row height — a 33-DIP repro breaks at ~63,550 rows instead).
- **[microsoft/microsoft-ui-xaml#1876](https://github.com/microsoft/microsoft-ui-xaml/issues/1876)** —
"ItemsStackPanel extends beyond containing Grid" (**closed *not planned*, 2020**). Same area —
content escaping its container past a large item count (~30,000+) — but framed as a layout problem,
never root-caused, and closed without investigation. This report adds a precise reproducible
threshold (2²¹ DIP), the layout-correct / composition-clip-escape distinction, scale-independence,
and a root-cause hypothesis.

Contributor guide

Open the contributing guide

Research direction

Start with the attached ClipEscapeRepro and reproduce the 60,000-versus-70,000-row boundary at both display scales. Then trace the ScrollViewer content presenter, InsetClip, and ItemsStackPanel viewport or realization clip where the bottom boundary is computed from the extent. Done means partially visible rows remain clipped to the ListView bottom for large extents without affecting layout.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
desktop, frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.