Skip to content

feat(vulkan): wolf:vulkan image (native Vulkan NV12 encode) + Fedora switch - #450

Draft
JBailes wants to merge 23 commits into
stablefrom
feat/vulkan-image
Draft

feat(vulkan): wolf:vulkan image (native Vulkan NV12 encode) + Fedora switch#450
JBailes wants to merge 23 commits into
stablefrom
feat/vulkan-image

Conversation

@JBailes

@JBailes JBailes commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Draft. Builds and publishes a wolf:vulkan image with native Vulkan NV12 H.264 encode, on a Fedora base. Pairs with games-on-whales/gst-wayland-display#37, which publishes the gst-wayland-display:vulkan base image this builds FROM.

What this adds

1. Native Vulkan NV12 H.264 encode (feat(vulkan))
A VULKAN encoder type wired through config/streaming: a vulkanh264enc h264_encoders block, producer_buffer_caps = video/x-raw(memory:VulkanImage), format=NV12, and video_params_zero_copy = "queue" so the encoder consumes the producer's NV12 VulkanImage directly (no videoscale/videoconvert/videorate, which can't negotiate the memory:VulkanImage feature). waylanddisplaysrc vulkan=true is set when the buffer caps ask for VulkanImage. Tunables: WOLF_VULKAN_QUALITY / _REF_FRAMES / _B_FRAMES (defaults tuned for low-latency: quality=0 num-ref-frames=1 b-frames=0).

2. wolf:vulkan image (ci)
docker/wolf.vulkan.Dockerfile is FROM ghcr.io/games-on-whales/gst-wayland-display:vulkan — which already carries patched GStreamer 1.28.4 (vulkan-video + the vulkanh264enc DPB-pool patch) and the plugin under /opt/gst. So the heavy gst-Vulkan + plugin build happens once in that image; here we only compile Wolf on top. A Build Wolf (Vulkan) step in docker-build.yml publishes ghcr.io/<owner>/wolf:vulkan.

3. Fedora image switch (feat(fedora))
Moved here from #431 per request. gpu-drivers, gstreamer and wolf images build on Fedora (base-app:fedora) instead of Ubuntu/apt. Wolf runs its own PulseAudio under supervisord (the Fedora base ships a conflicting pipewire-pulseaudio shim, swapped with --allowerasing), with graceful SIGINT shutdown to match stopsignal=INT.

Relationship to other PRs

Status / testing

Hardware-validated end-to-end on an AMD RX 7900 XTX (RADV + RPM Fusion freeworld mesa): waylanddisplaysrc → NV12 VulkanImage → vulkanh264enc → Moonlight streams real content. Draft pending CI image builds and broader GPU validation.

JBailes added a commit that referenced this pull request Jun 27, 2026
Per review, keep this PR focused on the image smoke-test harness and the
Wayland-socket-wait feature + tests. The wholesale Ubuntu->Fedora image switch
(gpu-drivers/gstreamer/wolf Dockerfiles, embedded PulseAudio + supervisord, the
Fedora devcontainer) is moved to the Wolf vulkan draft PR (#450), where it pairs
with the native-Vulkan wolf:vulkan image. Reverts those files to stable here;
the boost::json migration, the pulse ready-state guard and the sign.hpp/cstdint
compile fix stay (distro-independent).
JBailes added 3 commits June 27, 2026 06:30
Add a VULKAN encoder type wired through the config/streaming path:
- VULKAN enum + encoder_type("vulkan") and a vulkanh264enc h264_encoders block
- producer_buffer_caps video/x-raw(memory:VulkanImage),format=NV12 for the
  zero-copy path, and video_params_zero_copy="queue" so the encoder consumes the
  producer's NV12 VulkanImage directly (no videoscale/convert/rate, which can't
  negotiate the VulkanImage memory feature)
- waylanddisplaysrc vulkan=true when the buffer caps ask for VulkanImage
- WOLF_VULKAN_QUALITY / _REF_FRAMES / _B_FRAMES tunables (defaults tuned for
  low-latency streaming: quality=0, num-ref-frames=1, b-frames=0)
Move gpu-drivers, gstreamer and wolf images from Ubuntu/apt to Fedora/dnf,
building FROM ghcr.io/games-on-whales/base-app:fedora (the rolling GOW Fedora
tag). Wolf runs its own PulseAudio server supervised by supervisord (the Fedora
base ships a conflicting pipewire-pulseaudio shim, swapped out with
--allowerasing), removing the external WolfPulseAudio sidecar and its startup
race. wolf.cpp gains graceful SIGINT shutdown to match supervisord's
stopsignal=INT/stopwaitsecs. Bundles the linked libicu and registers
/usr/local/lib64 for the gst plugin's runtime libs.

Moved out of #431 so that PR keeps only the smoke-test harness + Wayland
resolution tests.
docker/wolf.vulkan.Dockerfile builds Wolf on top of
ghcr.io/games-on-whales/gst-wayland-display:vulkan (patched GStreamer 1.28.4
vulkan-video + the plugin under /opt/gst), so the heavy gst-Vulkan + plugin
build happens once in that image and here we only compile Wolf. A "Build Wolf
(Vulkan)" step in docker-build.yml publishes ghcr.io/<owner>/wolf:vulkan.
@JBailes
JBailes force-pushed the feat/vulkan-image branch from d4d00b2 to bbdac70 Compare June 27, 2026 06:35
JBailes added a commit that referenced this pull request Jun 27, 2026
Add bin/test-image.sh — a 3-layer smoke test for the built wolf image:
structural (docker inspect), cont-init smoke, and docker/tests/smoke.sh (wolf +
fake-udev present and ldd-clean, the custom waylanddisplaysrc plugin loads, key
env vars set), with bin/tests/lib.sh assertion helpers. Wired into
docker-build.yml: PR builds load the image locally and run the harness when
registry secrets are unavailable.

Pared down to just the smoke harness — the Fedora switch moved to #450, and the
embedded-PulseAudio (#422), wayland-socket-wait (#418), pulse fix (#420) and
boost::json migration have since merged to stable independently.
JBailes and others added 20 commits June 27, 2026 15:46
… encoder

When an AMD GPU is paired with a Vulkan Video encoder, enable RADV's
low-latency encode mode (Mesa 26.1+) on Wolf's own process environment,
where the encoder pipeline runs. User-set RADV_PERFTEST is left untouched.
RADV's Vulkan HEVC encoder corrupts the bottom/right partial coding-tree
block whenever a stream dimension isn't a multiple of the 64px CTB (every
standard height: 1080/720/2160) — a noise/green bar across that edge,
while H.264 and the other HEVC encoders are unaffected.

When the selected HEVC pipeline is vulkanh265enc, round the requested
resolution up to whole CTBs so the encoder only ever sees full coding
blocks; the client scales the slightly-larger frame back to its viewport.
Scoped to the vulkanh265enc pipeline so other encoders keep their exact
requested resolution.
WOLF_HDR=TRUE selects the P010 (10-bit) producer buffer format on the native
Vulkan path; the producer pipeline then passes hdr=true to waylanddisplaysrc
(BT.2020 PQ + mastering-display/content-light metadata), and vulkanh265enc
emits a Main-10 HDR10 stream. Default stays 8-bit NV12 so SDR clients are
never mis-signaled as HDR. Mirrors the existing vulkan=true producer prop.
Force a clean recompile so wolf:vulkan provably layers on the latest
gst-wayland-display:vulkan (producer P010+HDR, encoder HDR SEI, vulkanh265enc
patch) rather than a stale cached base from an earlier interleaved build.
…o] hdr)

Replace the WOLF_HDR env toggle with a reflect-cpp config field
GstVideoCfg.hdr (default false). [gstreamer.video] hdr = true selects the
P010 10-bit producer format -> producer hdr=true (BT.2020/PQ) -> vulkanh265enc
HDR10 Main-10. Config-driven so a managed/SmoothNAS deploy can enable it via
the host-mounted config.toml instead of a container env var.
The [gstreamer.video] hdr=true toggle previously only switched the producer
to P010, which broke HEVC entirely: the encoder pipeline's downstream
capsfilter pins profile=main (8-bit), so vulkanh265enc rejected the 10-bit
P010 input with 'No valid profile found' -> not-negotiated -> no video. And
Wolf never advertised Main-10, so Moonlight warned 'host doesn't support HDR'
and connected SDR/BT709 (mis-decoding a PQ stream).

This wires the two missing handshake pieces, both gated on the hdr config:
- serverinfo now ORs VIDEO_FORMAT_H265_MAIN10 into ServerCodecModeSupport
  (new Config::support_hdr) so Moonlight offers the HDR toggle and connects
  in BT2020/PQ mode.
- the vulkan HEVC pipeline's profile=main capsfilter is rewritten to
  profile=main-10 so the encoder can negotiate the P010 Main-10 path.
… bar)

The 64px CTB rounding that works around RADV's partial-block corruption was
only applied to the encoder dimension in rtsp/commands.hpp (built fresh from
the RTSP clientViewport). But the Vulkan producer renders the virtual
compositor at StreamSession.display_mode, which is set earlier at /launch from
the unrounded 'mode' header. So at a standard 1080p request the compositor
filled 1080 rows while the encoder read full 1088 (17x64) CTBs -- the extra 8
rows were uninitialised P010 (~green), showing as a green bar along the bottom.
It only looked clean when the client requested a literal 64-aligned height.

Round display_mode at /launch for the Vulkan producer so the compositor matches
the encoder, and extend the encoder-side rounding to vulkanh264enc too (CTB-
immune, but it shares the now-rounded Vulkan compositor so its encode dimension
must agree).
New [gstreamer.video] sdr_reference_white (default 203 nits) is exported as
WOLF_SDR_REFERENCE_WHITE when hdr=true, feeding the producer shader's
specialization constant. Tune SDR-content brightness in the HDR stream by
editing config.toml + restarting wolf -- no rebuild.
The producer posts a 'wolf-hdr-state' application message (bool 'hdr') on HDR<->SDR
content change; streaming.cpp catches it -> HDRModeEvent -> control.cpp sends the
HDR_MODE (0x010e) control packet (ControlHdrModePacket + SS_HDR_METADATA, BT.2020/
1000-nit/CLL 1000:400) to the client so its display flips HDR mode. Change-only.
Foundation for Model B dynamic switching; the producer-side caps flip (P2.3) pairs
with this.
…-copy)

Adds a Vulkan Video HEVC encoder to the default hevc_encoders list, mirroring the existing
vulkanh264enc entry. Unlike the nvcodec/va entries it omits the profile=main capsfilter, so
the 10-bit P010 Main-10 HDR path negotiates (waylanddisplaysrc vulkan=true emits NV12/P010
memory:VulkanImage; zero-copy params are just a decoupling queue). rate-control=cqp; tunables
from WOLF_VULKAN_QUALITY/_REF_FRAMES/_B_FRAMES.

Listed first so the zero-copy HDR path is preferred where available; check_elements gates it
out on Mesa/RADV (no HEVC vulkan encode yet) so Wolf falls through to nvcodec/va. Validated
end-to-end on RTX 5080 (Blackwell): 'Using h265 encoder: vulkan'.
…ISABLE)

The vulkanh26x encoder creates its own gst.vulkan.instance at session start; on mixed-GPU
hosts that dlopen()s lavapipe whose libLLVM collides with the mesa-GLES libLLVM -> abort/
std::terminate (seen as a boost-mutex flood + SIGABRT on session start). The producer plugin's
plugin_init already sets this, but the image ENV guarantees it for the encoder instance too,
independent of plugin load order. Confirmed: Steam + FF7 Rebirth stream on RTX 5080 with it set.
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.

1 participant