No description
  • C 75.3%
  • Go 22.3%
  • Shell 2.1%
  • Makefile 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-27 13:52:34 +08:00
cmd/portal-view Working commit with proper scaling and window handling 2026-08-27 12:38:21 +08:00
docs Toast improvements 2026-08-27 13:47:17 +08:00
internal Fix the shim 2026-08-27 13:52:34 +08:00
native Toast improvements 2026-08-27 13:47:17 +08:00
scripts Toast improvements 2026-08-27 13:47:17 +08:00
.gitignore Initial bake testing without buf display or display enumeration 2026-08-26 21:47:52 +08:00
CHANGES-2026-08-27.md Toast improvements 2026-08-27 13:47:17 +08:00
CODEX_HANDOFF.md Toast improvements 2026-08-27 13:47:17 +08:00
go.mod Initial bake testing without buf display or display enumeration 2026-08-26 21:47:52 +08:00
go.sum Initial bake testing without buf display or display enumeration 2026-08-26 21:47:52 +08:00
LICENSE Initial commit 2026-08-26 13:05:27 +00:00
Makefile Working commit with proper scaling and window handling 2026-08-27 12:38:21 +08:00
README.md Toast improvements 2026-08-27 13:47:17 +08:00

portal-view

A small Wayland utility-in-progress for mirroring one portal-selected application window onto another local Wayland surface.

The intended path is:

KWin window
   -> xdg-desktop-portal-kde WINDOW picker
   -> restricted PipeWire stream
   -> DMA-BUF
   -> EGLImage / GL texture
   -> Wayland viewer
   -> physical or KWin virtual output
   -> optionally Sunshine/Moonlight

The hot path is designed to avoid copying frame pixels into Go memory. Go owns CLI/session orchestration; a compact native PipeWire/EGL/Wayland shim owns frame negotiation/import/presentation.

Status

The portal selection path works: portal-view connects to xdg-desktop-portal ScreenCast over D-Bus, shows the compositor's window picker, receives the restricted PipeWire socket FD and stream identity (v6 pipewire-serial with v5 node-ID fallback), and supports --save/--restore restore tokens.

The native build now negotiates KWin's BGRA/BGRx shared-memory stream, uploads frames through EGL/GLES2, and displays them in a regular resizable xdg-toplevel window. DMA-BUF import remains a later optimization.

make native
./bin/portal-view

The no-flag command opens the portal window picker, then presents the selected window in a regular resizable portal-view window. --fullscreen requests fullscreen mode; --output NAME selects the fullscreen output when supported. Use --list-outputs to list compositor outputs without opening the portal.

Viewer keys:

  • q or Escape closes the viewer.
  • f toggles compositor-managed borderless fullscreen; pressing it again restores the regular window.
  • 1..9 draw the frame at that integer pixel ratio (1:1, 2x, ...), centered and cropped when larger than the window.
  • 0 restores fit-to-window scaling.
  • s cycles the scaling filter: auto (nearest for integer zoom, bilinear for fit), nearest, bilinear, then bicubic (Catmull-Rom).

Scaling/fullscreen key changes also display a short mpv-style on-screen status toast (for example scaling mode: bicubic or scaling locked: 1:1). Integer zoom uses an integer framebuffer viewport; 1 therefore has an exact 1 source pixel -> 1 output pixel mapping in the viewer itself.

If the selected source changes size, PipeWire may renegotiate the capture frame dimensions; the viewer updates its texture and preserves the current destination window size. The compositor owns xdg-toplevel sizing, so source resizing does not automatically resize the viewer window.

On fractional-scale Wayland outputs (e.g. 125%), the viewer negotiates fractional-scale-v1 and viewporter so its EGL buffer matches physical pixels; 1 then maps each captured pixel to one physical pixel. Note that KWin may already have resampled the source window while producing the portal stream, so this cannot reconstruct font pixels lost before portal-view receives them.

For Fedora development dependencies:

./scripts/fedora-dev-deps.sh
./scripts/check-host.sh

Then hand the entire repository to a coding agent with CODEX_HANDOFF.md as the starting brief.

Running the native build

The real binary (portal picker + PipeWire stream) needs the native dev packages and the native build tag:

make native     # tests + build with -tags native

The current native viewer uses KWin-compatible MemFd/SHM negotiation for a reliable first working path. DMA-BUF remains planned: the final zero-copy path will query EGL's importable DRM formats/modifiers rather than maintaining a hard-coded AMD/Intel modifier list.

If the PipeWire stream errors, the portal error message is logged; for deep PipeWire-side diagnostics run with PIPEWIRE_DEBUG=2.

DMA-BUF roadmap

Useful debugging switches already reserved by the CLI:

portal-view --force-shm      # conservative SHM path today
portal-view --no-modifiers   # reserved for the DMA-BUF implementation
portal-view -v

--no-modifiers is intentionally a no-op in the current native renderer; it is kept so the DMA-BUF test matrix and eventual native negotiation use the same CLI. The current working capture path is MemFd/SHM.

KDE virtual output + Sunshine

See docs/KDE_VIRTUAL_MONITOR.md.

The repo now includes scripts/kde-virtual-output.sh to create/detect/tear down a named-resolution virtual output without guessing KWin's assigned output name. The important part is that krfb-virtualmonitor creates a compositor-level KWin output. Sunshine therefore needs:

capture = kwin
output_name = Virtual-SunshineHeadless

rather than KMS capture.

Development

make test
make fuzz
make check
make handoff

The DMA-BUF-specific verification plan lives in docs/DMA_BUF_TEST_MATRIX.md.