Skip to content

fix(win32): window-space UI HUDs clipped by the SetWindowRgn silhouette (#350) - #351

Merged
dfattal merged 2 commits into
mainfrom
fix/win-region-includes-hud
Oct 2, 2026
Merged

dfattal merged 2 commits into
mainfrom
fix/win-region-includes-hud

Conversation

@dfattal

@dfattal dfattal commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes #350.

Problem

SetWindowRgn is a visual clip, not only a hit mask. The transparent overlay's region is built around the avatar silhouette, so any window-space UI HUD (DisplayXRWindowSpaceUI → XrCompositionLayerWindowSpaceDXR) off the silhouette was cut away.

Fix (native~/displayxr_win32.c, Windows-only)

A new helper, wsui_region_rects, turns every live wsui slot into a client-pixel rect. That rect is unioned into every region the overlay builds:

No app call is needed. This mirrors the Linux input region in #346 (lin_rects_add_wsui) and the model viewer's "chrome" rects (displayxr-common vk_clickthrough_region.h).

Frame math (and why it differs from #346)

A slot's x/y/w/h/disparity are fractions of the whole window client, not of zone 0. I checked this against displayxr-runtime origin/main @ de3f621:

  • The layer is drawn into each per-view atlas tile at tile_origin + (x ± disparity/2) · tile_w. See comp_d3d12_renderer.cpp render_window_space_layer / comp_d3d12_renderer_draw_window_space_pass, comp_d3d11_renderer.cpp render_window_space_layer (fraction → NDC of the view viewport), and vk_native/comp_vk_native_compositor.c ~2660 (dx = tile_origin_x + (ws->x + eye_shift) * tile_w).
  • The tile spans the full window in both frame kinds the provider submits:
    • A plain projection frame has no effective canvas, so the weave uses the full-target fallback (d3d12_effective_canvas).
    • A zones frame, which the provider sends whenever a 3D zone is set, composes each zone into a window-spanning tile. The source says: "in zones frames the tile spans the full window" (comp_d3d12_renderer.cpp ~3245, comp_d3d11_renderer.cpp ~1897).

So #346's zone-0 frame matches only when there is no sub-rect zone. With one (the avatar's bubble band sits beside its zone), it would misplace the HUD rects. #346 probably wants the same correction.

Each eye is shifted by ±disparity/2 (graded across views for more than 2, with the same extremes). The rect is therefore widened by |disparity|/2 on each side, rounded outward and clamped to the client.

DPI space

The HUD rect is scaled by the same client size the silhouette uses. On the mask path that is dst_w/dst_h. On the hit-rect path it is displayxr_get_overlay_size, the same GetClientRect C# used for the AABB, called on the same thread that calls SetWindowRgn. It never uses managed Screen.*, so the HUD rects share a space with the rest of the region whatever DPI awareness that thread has.

Hidden HUDs are excluded

displayxr_window_space_ui_get_pending_slot returns 1 only while a texture is registered, and release_slot/clear_slot null that texture. That is exactly when the provider submits the layer. A HUD that is disabled (OnDisable → ReleaseSlot), or that was never enabled (the app launches with HUDs off and toggles them with Shift+Tab), is not in the region. A HUD hidden only by blanking its canvas while the component stays enabled is still submitted, so it still counts.

Threads and cost

The slot registry is written from managed on the main thread. Both region builders also run on the main thread (LateUpdate and the AsyncGPUReadback callback), so there is no cross-thread read. No provider state is read and no window query is added on the mask path. The work is at most DXR_WSUI_MAX_SLOTS (4) volatile reads per rebuild.

Clicks

Clicks inside a HUD reach the app as a result of the union: the borderless top-level overlay's WM_NCHITTEST returns HTCLIENT for every hit the OS delivers. s_hit_rect is consulted only on the opaque WS_CHILD path.

Also

Verification

  • MSVC build is clean.
  • Hardware verification pending. A human A/B was running on the panel, so nothing was launched. To check: in an avatar app with HUDs, (1) launch with HUDs off and confirm the region is unchanged; (2) press Shift+Tab and confirm the HUD is fully visible off the silhouette and clickable; (3) hide it and confirm the area clicks through to the desktop again; (4) with a static avatar and a HUD shown, confirm the region is not re-applied every frame (the hit_mask log stays quiet apart from the 1 Hz line).

🤖 Generated with Claude Code

…350)

SetWindowRgn is a visual clip, not only a hit mask, so a window-space UI
HUD off the avatar silhouette was cut away. Every region the transparent
overlay builds (hit mask + surround union, and the AABB hit-rect path
before the first mask) now unions the client-pixel rect of each live wsui
slot, natively and with no app call, mirroring the Linux input region
(#346).

The fractions are of the whole window client, not zone 0. The runtime
draws the layer into each per-view tile, and the tile spans the full
window in both plain-projection and zones frames. The rect is widened by
|disparity|/2 per side and scaled by the same client size the silhouette
uses. A slot counts only while a texture is registered, so a disabled HUD
drops out. HUD rects feed the region hash (and bypass the hit-rect
hysteresis), so toggling re-applies the region and a static HUD costs no
SetWindowRgn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…from the merged source

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dfattal
dfattal marked this pull request as ready for review October 2, 2026 09:01
@dfattal
dfattal merged commit 3af86bf into main Oct 2, 2026
8 checks passed
@dfattal
dfattal deleted the fix/win-region-includes-hud branch October 2, 2026 09:01
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.

Windows transparent overlay: window-space UI HUDs clipped by the SetWindowRgn silhouette

1 participant