feat(clients/windows): all-vendor video pipeline rewrite + app icon + hosts-page tiles

Decode+present rewrite (first real pixels on glass for this client):

- Decode: FFmpeg D3D11VA on NVIDIA/AMD/Intel. get_format now only returns
  AV_PIX_FMT_D3D11 and lets libavcodec build the decode pool from
  hw_device_ctx (hand-built frames contexts failed three different ways:
  NVIDIA rejects DECODER|SHADER_RESOURCE arrays, BindFlags=0 fails texture
  creation, Intel rejects non-128-aligned HEVC surfaces at the first
  SubmitDecoderBuffers). A DXVA profile probe before the hwdevice commits
  hardware-vs-software up front instead of burning the opening IDR;
  extra_hw_frames covers the frames the client holds.
- Present: the decoded slice is copied with ONE display-size-boxed
  CopySubresourceRegion (a planar slice is a single subresource in D3D11;
  the old two-copy D3D12-style code silently no-opped - the black screen)
  into a sampleable NV12/P010 texture, per-plane SRVs + YUV->RGB shaders.
- New dedicated render thread (render.rs): presenting is decoupled from the
  XAML thread; frame-latency-waitable swapchain + SetMaximumFrameLatency(1),
  newest-wins drain after the wait, crossbeam frame channel with pts for a
  capture->presented p50 log.
- HiDPI: pixel-sized buffers + SetMatrixTransform(96/dpi) - was blurry at
  125/150 % scaling.
- Software fallback now feeds the same shaders (swscale -> NV12/P010 planes
  -> two dynamic plane textures); ps_rgba/X2BGR10 path deleted, hw/sw colour
  math identical.
- Adapter selection for hybrid boxes: PUNKTFUNK_ADAPTER > the window's
  monitor's adapter > default; PUNKTFUNK_D3D_DEBUG=1 debug layer.
- Session pump: request_keyframe at start and on hw->sw demotion (infinite
  GOP would otherwise sit on a black screen).

Validated live on the Arc Pro + RTX 3500 Ada laptop against the local
Windows host: 60 fps D3D11VA on both vendors, software path, GUI on glass.

Also: embedded app icon (build.rs winresource + WM_SETICON, MSIX
Square44x44 targetsize assets, pack-msix stages them) and the hosts-page
tile rework (tap-to-connect tiles with sibling overflow menu - fixes
forget-also-connects - in-tile rename editor, add-host modal via root state).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 16:24:23 +02:00
parent 03ad2406b6
commit 1f5a2e4920
36 changed files with 1797 additions and 581 deletions
+70 -8
View File
@@ -27,26 +27,67 @@ pub(crate) fn card(child: impl Into<Element>) -> Border {
.padding(uniform(16.0))
}
/// Card chrome with no padding — for cards whose interactive regions (tap-to-connect area vs.
/// action buttons) must own their padding so hit areas reach the card edges.
pub(crate) fn card_flush(child: impl Into<Element>) -> Border {
card(child).padding(uniform(0.0))
}
/// An OPAQUE modal/dialog surface. `card`'s `CardBackground` is a translucent acrylic brush — fine
/// layered on the page, but a floating dialog over a scrim needs a solid fill or the content behind
/// bleeds through (looks "transparent"). `SolidBackground` is the opaque base-layer brush.
pub(crate) fn dialog_surface(child: impl Into<Element>) -> Border {
border(child.into())
.background(ThemeRef::SolidBackground)
.border_brush(ThemeRef::SurfaceStroke)
.border_thickness(uniform(1.0))
.corner_radius(8.0)
.padding(uniform(20.0))
}
/// A fully transparent brush: paints nothing but (unlike a null background) makes the whole
/// element hit-testable, so a tap region catches clicks in its blank space too.
pub(crate) fn hit_test_backstop() -> Color {
Color {
a: 0,
r: 0,
g: 0,
b: 0,
}
}
/// A small all-caps section label above a group of cards.
pub(crate) fn section(label: &str) -> Element {
text_block(label)
.font_size(12.0)
.semibold()
.foreground(ThemeRef::SecondaryText)
.margin(edges(2.0, 10.0, 0.0, 0.0))
.margin(edges(2.0, 14.0, 0.0, 2.0))
.into()
}
/// Wrap a screen's children in a scrollable, centred, max-width column.
/// Wrap a screen's children in a scrollable, centred, max-width column. Alignment stays the
/// default Stretch: with a MaxWidth that still centres the column, but the children get the
/// column's REAL width — an explicit Center would size the column to its content and leave
/// every card at its minimum width no matter how large the window is.
pub(crate) fn page(children: Vec<Element>) -> Element {
let col = vstack(children)
.spacing(10.0)
.max_width(640.0)
.horizontal_alignment(HorizontalAlignment::Center)
.margin(edges(24.0, 24.0, 24.0, 40.0));
scroll_view(col).into()
}
/// Like [`page`], but wide and airier — for screens whose cards lay out in a responsive grid
/// and should use the window instead of a narrow settings column.
pub(crate) fn page_wide(children: Vec<Element>) -> Element {
let col = vstack(children)
.spacing(14.0)
.max_width(1120.0)
.margin(edges(32.0, 28.0, 32.0, 48.0));
scroll_view(col).into()
}
/// A page header: a large bold title on the left, one action button on the right.
pub(crate) fn page_header(title: &str, action: Button) -> Element {
grid((
@@ -103,7 +144,9 @@ pub(crate) fn avatar(name: &str) -> Border {
text_block(initial)
.font_size(17.0)
.semibold()
.foreground(ThemeRef::AccentText)
// NOT ThemeRef::AccentText — that's accent-COLOURED text for normal surfaces;
// on an accent fill it's accent-on-accent (unreadable). This is the on-accent brush.
.foreground(ThemeRef::custom("TextOnAccentFillColorPrimaryBrush"))
.horizontal_alignment(HorizontalAlignment::Center)
.vertical_alignment(VerticalAlignment::Center),
)
@@ -116,20 +159,39 @@ pub(crate) fn avatar(name: &str) -> Border {
/// Pill chip colour intent.
#[derive(Clone, Copy)]
pub(crate) enum Pill {
Accent,
Info,
Good,
Neutral,
}
/// A small rounded status chip (paired/PIN/HDR/etc.).
/// A small rounded status chip (paired/PIN/HDR/etc.) — subtle tinted fills with matching
/// system foregrounds (the InfoBar palette), never solid accent (white-on-bright is unreadable).
pub(crate) fn pill(text: &str, kind: Pill) -> Border {
let (bg, fg) = match kind {
Pill::Accent => (ThemeRef::Accent, ThemeRef::AccentText),
Pill::Info => (
ThemeRef::SystemAttentionBackground,
ThemeRef::SystemAttention,
),
Pill::Good => (ThemeRef::SystemSuccessBackground, ThemeRef::SystemSuccess),
Pill::Neutral => (ThemeRef::SubtleFill, ThemeRef::SecondaryText),
};
border(text_block(text).font_size(11.0).semibold().foreground(fg))
.background(bg)
.border_brush(ThemeRef::CardStroke)
.border_thickness(uniform(1.0))
.corner_radius(10.0)
.padding(edges(9.0, 3.0, 9.0, 3.0))
.padding(edges(9.0, 2.0, 9.0, 2.0))
}
/// A small presence dot (host online/offline).
pub(crate) fn presence_dot(online: bool) -> Border {
border(vstack(Vec::<Element>::new()))
.background(if online {
ThemeRef::SystemSuccess
} else {
ThemeRef::SystemNeutral
})
.corner_radius(4.0)
.width(8.0)
.height(8.0)
}