Skip to content

Fixes #5360. Cull fully occluded overlapped opaque subviews during draw#5477

Merged
harder merged 2 commits into
tui-cs:developfrom
harder:fix-5360-cull-occluded-subviews
Jun 11, 2026
Merged

Fixes #5360. Cull fully occluded overlapped opaque subviews during draw#5477
harder merged 2 commits into
tui-cs:developfrom
harder:fix-5360-cull-occluded-subviews

Conversation

@harder

@harder harder commented Jun 9, 2026

Copy link
Copy Markdown
Member

Summary

DrawSubViews() now skips drawing an Overlapped sibling when it is entirely covered by the higher-Z opaque peers already drawn. A fully-covered sibling produces no visible output - every cell it would draw (including its own RenderLineCanvas) is clipped away by the clip "holes" those peers punch in DoDrawComplete - so skipping its Draw() is output-neutral. This is the occlusion-culling step of the #4973 series, taken now that the invalidation semantics are stable (#5357, #5358, #5359, #5433, #5434).

The culled view's NeedsDraw is cleared (mirroring a drawn-but-fully-clipped pass) so it does not keep the SuperView perpetually dirty.

Approach

  • Occlusion is tracked with a Region of the screen area opaquely covered by higher-Z peers, accumulated as the existing reverse-Z (top-first) draw loop iterates.
  • A new IsOpaqueForOcclusion(out Rectangle) reports whether a view fills a screen rectangle opaquely. The opaque region is the area inside the (transparent-by-default) Margin - Border.FrameToScreen() - matching exactly what DoDrawComplete excludes from the clip.
  • IsFullyCovered(rect, coverage) uses Region difference (not single-rect Contains) so coverage spread across multiple peers is handled correctly.
  • The whole check is gated on the presence of an Overlapped sibling, so the common (non-overlapping) draw path is zero-overhead.

Why it's conservative (safety guards)

A sibling is culled only when all of these hold. Each guard maps to a hazard called out in the issue:

Guard Hazard it avoids
candidate is Overlapped scope = overlapped siblings only
opaque (View/Border/Padding not Transparent) ViewportSettings.Transparent - transparent views don't fully cover
ShadowStyle is None margin shadows draw in a separate second pass, on top
!SuperViewRendersLineCanvas line-canvas composition / reserved cells - this is what keeps Tabs correct: tab pages render their borders (and headers) through the SuperView's painters'-algorithm LineCanvas composition, so they must never be culled
full frame ⊆ accumulated opaque coverage partial coverage still draws

Tabs pages are SuperViewRendersLineCanvas, so they are deliberately not culled - preventing any header-line-art regression while still benefiting plain overlapped opaque siblings (overlapped Windows/panels, Arrangement-style stacks).

Tests

  • New Tests/UnitTestsParallelizable/ViewBase/Draw/OcclusionCullingTests.cs (11 tests, all green): fully-occluded cull, NeedsDraw clearing, partial coverage, transparent occluder, transparent candidate, shadowed candidate, SuperViewRendersLineCanvas candidate, non-Overlapped candidate, the no-Overlapped-siblings gate, output-neutrality, and a Tabs regression asserting inactive pages are never culled.
  • Verification uses the headless in-process driver and DrawComplete event counting (which fires iff Draw() was invoked), not just Driver.Contents.
  • Full suite green: 17,370 parallelizable + 74 non-parallelizable tests pass. No new build warnings.

Visual & output-neutrality testing (tuirec + PTY)

Beyond the unit tests, I verified at the real-terminal layer (addressing the Driver.Contents ≠ IOutput caveat) using Scripts/tuirec recordings and tmux capture-pane frame diffs on the scenarios most likely affected:

  • Tabs Example - overlapped SuperViewRendersLineCanvas pages (the guarded case)
  • Arrangement - overlapped opaque views (where culling actually fires)
  • WideGlyphs - overlapped views + clipping

Method: built the app with and without the change, drove each scenario identically under a PTY, and diffed the settled screen grids. A control (capturing the same build twice) isolates the scenarios' own non-determinism.

Scenario before (no cull) vs after (cull) same build, run twice (control) Result
Tabs 0 differing lines 0 byte-for-byte identical
Arrangement 2 differing lines 2 diff == its animated progress bar only
WideGlyphs 60 differing lines 60 diff == its random wide-glyph fill only

Each before/after difference exactly equals that scenario's own run-to-run variance - culling adds nothing - and the regression-risk case (Tabs) is identical. Recordings:
tabs-after
arrangement-after
wideglyphs-after

To pull down this PR locally:

git remote add copilot https://github.com/harder/Terminal.Gui.git
git fetch copilot fix-5360-cull-occluded-subviews
git checkout copilot/fix-5360-cull-occluded-subviews

harder and others added 2 commits June 9, 2026 00:15
…ing draw

DrawSubViews() now skips drawing an Overlapped sibling when it is entirely
covered by the higher-Z opaque peers already drawn. Such a sibling produces
no visible output — every cell it would draw, including its own
RenderLineCanvas, is clipped away by the clip "holes" those peers punched in
DoDrawComplete — so skipping its Draw is output-neutral. The culled view's
NeedsDraw is cleared (mirroring a drawn-but-fully-clipped pass) so it does not
keep the SuperView perpetually dirty.

Culling is intentionally conservative — it only applies to a candidate that
is itself Overlapped, opaque (content/Border/Padding not Transparent), without
a Margin shadow, and NOT SuperViewRendersLineCanvas. The last guard is what
keeps Tabs correct: tab pages render their borders (and headers) through the
SuperView's LineCanvas painters'-algorithm composition, so they must never be
culled or their header line art would be dropped. The whole check is gated on
the presence of an Overlapped sibling, keeping the common non-overlapping draw
path zero-overhead.

Opaque coverage is the region inside a view's (transparent-by-default) Margin —
the Border frame — which matches the area DoDrawComplete excludes from the clip.

Adds OcclusionCullingTests covering: fully-occluded cull, NeedsDraw clearing,
partial coverage, transparent occluder, transparent candidate, shadowed
candidate, SuperViewRendersLineCanvas candidate, non-Overlapped candidate, the
no-Overlapped-siblings gate, output-neutrality, and a Tabs regression that
inactive pages are never culled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cull path skips Draw()/DoDrawComplete(), which repopulates a view's
CachedDrawnRegion for ViewportSettingsFlags.TransparentMouse layers. That
region is invalidated by SetNeedsDraw(), so culling an opaque TransparentMouse
view left it with a null cache and dropped it from mouse hit-testing
(GetViewsUnderLocation blanket-removes TransparentMouse views with a null
cache) — a state divergence from the drawn-but-clipped path the cull is meant
to mirror.

Add ParticipatesInTransparentMouseHitTesting() and exclude such candidates:
the view's own ViewportSettings, plus any adornment that is TransparentMouse
with non-empty Thickness (adornment caches survive SetNeedsDraw(), but the
first draw must still populate a thick transparent-mouse adornment's cache).
Margin is TransparentMouse by default, so this only excludes views with an
actual Margin thickness — the common empty-margin overlapped view is still
culled.

Adds regression tests: TransparentMouseCandidate_IsNotCulled_AndKeepsHitRegion
(fails without the guard — CachedDrawnRegion is null) and
TransparentMouseMarginCandidate_IsNotCulled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

This PR implements conservative occlusion culling in View.DrawSubViews() to skip drawing fully covered opaque Overlapped sibling views, reducing redundant draw work in stacked-overlap scenarios (e.g., tab pages, overlapped panels/windows). It tracks accumulated opaque screen coverage from higher-Z peers and clears NeedsDraw on culled views to prevent persistent dirty-state churn.

Changes:

  • Add an occlusion-culling pass to View.DrawSubViews() using a Region of accumulated opaque coverage and a conservative set of guards (transparency, shadows, line-canvas composition, transparent-mouse hit-testing participation).
  • Introduce helpers in View.Drawing.cs to determine “opaque for occlusion”, transparent-mouse participation, and region-based “fully covered” testing.
  • Add a focused unit test suite validating culling behavior, safety guards, output-neutrality, and Tabs regression coverage.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
Terminal.Gui/ViewBase/View.Drawing.cs Adds region-based occlusion culling in DrawSubViews() plus helper methods to conservatively determine safe cull candidates.
Tests/UnitTestsParallelizable/ViewBase/Draw/OcclusionCullingTests.cs Adds coverage for fully-occluded culling, safety guards (transparency, shadows, line-canvas, transparent mouse), and output-neutrality including Tabs regression.

@harder harder marked this pull request as ready for review June 9, 2026 22:56
@harder harder requested a review from tig as a code owner June 9, 2026 22:56
@tig

tig commented Jun 10, 2026

Copy link
Copy Markdown
Member

@harder what'd you think of the tuirec experience?

@tig tig left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Lovely, as usual.

@harder harder merged commit e9e24e5 into tui-cs:develop Jun 11, 2026
13 checks passed
@harder harder deleted the fix-5360-cull-occluded-subviews branch June 12, 2026 21:53
@harder

harder commented Jun 12, 2026

Copy link
Copy Markdown
Member Author

@harder what'd you think of the tuirec experience?

@tig I like how easy it is to automate making screenshot videos now! I need to play with it more. I remember reading about it, but hadn't spent time trying it out yet until I was asking my agent to propose additional ways I could test and verify that this change didn't break any UI. It suggested using tuirec and proposed those 3 areas, then wired it all up. Very cool.

@tig

tig commented Jun 13, 2026

Copy link
Copy Markdown
Member

@harder what'd you think of the tuirec experience?

@tig I like how easy it is to automate making screenshot videos now! I need to play with it more. I remember reading about it, but hadn't spent time trying it out yet until I was asking my agent to propose additional ways I could test and verify that this change didn't break any UI. It suggested using tuirec and proposed those 3 areas, then wired it all up. Very cool.

Also, see the new-ish test infra that lets you provide Goldens as .ans files and then IDriver.StringToAnsi.

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.

Cull fully occluded overlapped opaque subviews during draw

3 participants