mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-20 01:35:19 +03:00
Three related fixes. Together they take HDR from unreachable to working
end to end on the XWayland path.
## XWayland was never started
Three lines had been commented out since the initial import: the call
that spawns XWayland, the guard that waits for it, and the `DISPLAY`
handed to the child. Every game therefore launched as a native Wayland
client. Nothing reported it -- the compositor still logged the X display
it was telling clients to point at, which is why it read as working.
That is also why HDR never fired. The colour space is signalled over a
protocol whose Vulkan layer lives in the game process and finds the
compositor through the X11 root window, so the one path able to carry it
was the one path no game was on. `ENABLE_GAMESCOPE_WSI` and `DXVK_HDR`
were already being set, which switched that layer on and then handed it
a
display it could not use.
Restoring the guard also fixes the ordering it was written for: the
launch
now happens after XWayland reports ready rather than ~40ms before it.
## Mesa was dropping every format we advertised alpha-only
Mesa tracks two flags per VkFormat -- one contributed by a format alpha
FourCC, one by its opaque FourCC -- and skips any format carrying only
one:
```c
if (!(disp_fmt->flags & WSI_WL_FMT_ALPHA) ||
!(disp_fmt->flags & WSI_WL_FMT_OPAQUE))
continue;
```
We advertised `ARGB8888` and `XRGB8888`, so `B8G8R8A8` survived and made
the list look like it was working. Everything else was alpha-only and
was
dropped in silence -- `ABGR8888` had been advertised all along while
`R8G8B8A8` never once appeared on a surface. Adding the opaque spellings
takes the surface from 6 formats to 21 and restores the three that carry
HDR.
The comment above that list claimed it was for XWayland DRI3 and that a
game swapchain format was independent of it. It was the opposite: the
list decides what a game can select, and deleting an entry removes that
format from every client.
## Verified against swapchains, not format lists
A client asking for `A2B10G10R10` + `HDR10_ST2084` now gets a swapchain
and the compositor is told colorspace `1000104008`; one asking for
`R16G16B16A16_SFLOAT` + scRGB linear gets `1000104002`. Previously both
were refused at creation -- the WSI layer re-checks the requested format
against the driver own surface list, so the colour space and the pixel
format arrive from two different places and only one was being supplied.
`apps/nescope/scripts/verify-hdr-formats.sh` asks what a client is
offered
from inside a child process, keeping the XCB and Wayland surfaces apart
since a game presents through the XCB one. The default mode guards both
halves of what the compositor controls; `--expect-layer` states the full
target and passes once a WSI layer is present. No new dependencies
(`vulkaninfo` + `python3`).
## Still open
HDR is XWayland-only, documented as a FIXME in `hdr.rs`. A WSI layer
binds
the swapchain factory on its own Wayland connection while a native
client
surface lives on the client one, and object IDs do not cross
connections.
The FIXME records the fix both reference implementations point at, and
the
trap to avoid when we take it: gating format injection on "the
compositor
supports HDR" rather than on being able to signal the surface hands a
client PQ pixels that arrive tagged as SDR, with nothing reporting an
error.
nescope ships no WSI layer of its own; the above was verified with the
stock gamescope one, which drives our protocol unmodified.
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR makes HDR-capable native Wayland presentation reachable, adds
the alpha/opaque DMA-BUF FourCC pairs Mesa requires, makes XWayland an
explicit compatibility mode, and adds an HDR surface-format diagnostic.
- Starts XWayland only with `--xwayland`, waits for readiness before
launching the child, and stops cleanly if startup fails or times out.
- Routes Proton through Wayland unconditionally while retaining
`DXVK_HDR` as an HDR-specific setting.
- Advertises paired alpha and opaque FourCC variants with portable
modifiers.
- Separates XCB and Wayland probe results, selects one hardware adapter,
and distinguishes probe failures from format regressions.
- Documents the limitations of the legacy gamescope WSI path and the
external-layer dependency.
<h3>Confidence Score: 5/5</h3>
The PR appears safe to merge; no outstanding correctness, security, or
repository-rule issue remains.
All previous findings are resolved in the current code, including the
XWayland failure lifecycle, removal of unsupported vendor modifiers,
corrected HDR documentation, reliable diagnostic failure handling,
per-GPU format selection, and unconditional Proton Wayland routing. The
changes since the previous review preserve diagnostic output handling
without introducing a new failure.
<h3>Important Files Changed</h3>
| Filename | Overview |
|----------|----------|
| apps/nescope/src/main.rs | Adds opt-in XWayland lifecycle handling,
readiness timeout, conditional DISPLAY propagation, and unconditional
Proton Wayland routing; the previous launch-environment finding is
fixed. |
| apps/nescope/src/state.rs | Stops the event loop on reported XWayland
startup failure and advertises portable paired alpha/opaque DMA-BUF
formats without vendor-specific modifiers. |
| apps/nescope/src/hdr.rs | Documents the working native Wayland HDR
path and accurately distinguishes it from the external, deliberately
disabled gamescope WSI route. |
| apps/nescope/scripts/verify-hdr-formats.sh | Adds a diagnostic that
keeps GPU and surface paths separate and now preserves the intended exit
behavior when filtered Vulkan diagnostics contain no matching lines. |
<h3>Flowchart</h3>
```mermaid
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Launch[nescope child launch] --> Mode{--xwayland?}
Mode -->|No| Wayland[Native Wayland surface]
Mode -->|Yes| Wait[Start and await XWayland]
Wait -->|Ready| XCB[XCB / XWayland surface]
Wait -->|Error or 10s timeout| Stop[Log failure and stop]
Wayland --> Formats[Paired alpha and opaque FourCCs]
Formats --> HDR[HDR10 and scRGB formats available]
XCB --> SDR[X11 compatibility path without native HDR]
Proton[Proton child] -->|PROTON_ENABLE_WAYLAND=1| Wayland
```
<sub>Reviews (9): Last reviewed commit: ["nescope/scripts: guard the
diagnostic
pi..."](f8bdd68f87)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=60368324)</sub>
**Context used:**
- Knowledge Base — [Compositor, display, and input
control](https://app.greptile.com/nestri/-/custom-context/knowledge-base/nestrilabs/nestri/-/docs/compositor-input.md)
- Knowledge Base — [Streaming host
runtime](https://app.greptile.com/nestri/-/custom-context/knowledge-base/nestrilabs/nestri/-/docs/streaming-runtime.md)
<!-- /greptile_comment -->
279 lines
13 KiB
Bash
Executable File
279 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Report the surface formats a client actually sees under nescope.
|
|
#
|
|
# HDR reaches a game as a (VkFormat, VkColorSpaceKHR) pair on its swapchain
|
|
# surface. Everything else -- the colour-management protocol, the dmabuf format
|
|
# list, the encoder's matrix and transfer tags -- is downstream of whether that
|
|
# pair was ever offered. So this asks the one question directly, from inside a
|
|
# real client process, using the loader's own enumeration rather than ours.
|
|
#
|
|
# It deliberately does not check pixels. verify-hdr.sh in nescapture does that,
|
|
# and the two are answering different questions: this one is "was HDR on the
|
|
# menu", that one is "did the samples come out where the standard says". A pass
|
|
# here and a fail there means we converted wrongly; a fail here means the game
|
|
# never had the option and anything downstream is moot.
|
|
#
|
|
# The two surfaces a client can be on need separate answers, so there are two
|
|
# modes:
|
|
#
|
|
# (default) The Wayland surface, which is where HDR actually comes
|
|
# from: Mesa pairs the colour spaces it learns from
|
|
# wp_color_manager_v1 with the pixel formats it derives from
|
|
# the dmabuf list. Guards both halves. Each is silently
|
|
# absent when broken, and the colour space alone is what made
|
|
# an 8-bit surface look like working HDR.
|
|
#
|
|
# --expect-layer The XCB surface, for diagnosing the legacy route only.
|
|
# Mesa offers no HDR colour space on XWayland at all, so the
|
|
# HDR pairs can only come from a WSI layer inside the game's
|
|
# process -- gamescope's approach, which predates Wayland
|
|
# colour management. nescope does not ship such a layer and
|
|
# does not enable one, because capture cannot see the colour
|
|
# space it hides and would tag PQ samples as BT.709. This
|
|
# mode passes only with a layer loaded, e.g.
|
|
#
|
|
# VK_ADD_IMPLICIT_LAYER_PATH=<dir> \
|
|
# apps/nescope/scripts/verify-hdr-formats.sh --expect-layer
|
|
#
|
|
# A failure here is the expected state, not a regression.
|
|
#
|
|
# Exit codes are distinct on purpose: 0 pass, 1 the formats are wrong, 2 the
|
|
# environment or the probe failed and this run measured nothing.
|
|
#
|
|
# Usage: apps/nescope/scripts/verify-hdr-formats.sh [--expect-layer]
|
|
set -euo pipefail
|
|
|
|
MODE="baseline"
|
|
NESCOPE_ARGS=()
|
|
if [ "${1:-}" = "--expect-layer" ]; then
|
|
MODE="layer"
|
|
# nescope no longer sets this, on purpose, so the legacy layer stays inert in
|
|
# normal use. This mode is the one place that wants it, so it opts in here
|
|
# rather than relying on the compositor to leave a switch flipped.
|
|
export ENABLE_GAMESCOPE_WSI=1
|
|
# And the route only exists on the XCB surface, which needs a server.
|
|
NESCOPE_ARGS=(--xwayland)
|
|
fi
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
|
WORK="$(mktemp -d)"
|
|
trap 'rm -rf "$WORK"' EXIT
|
|
|
|
: "${XDG_RUNTIME_DIR:=/run/user/$(id -u)}"
|
|
export XDG_RUNTIME_DIR
|
|
|
|
for tool in vulkaninfo python3; do
|
|
command -v "$tool" >/dev/null || { echo "missing required tool: $tool" >&2; exit 1; }
|
|
done
|
|
|
|
echo "building…"
|
|
cargo build --release -p nescope --manifest-path "$ROOT/Cargo.toml" >/dev/null
|
|
|
|
# The probe runs as nescope's child, so it sees exactly the environment a game
|
|
# would. Which display variables it inherits is itself a result -- a child with
|
|
# no DISPLAY is a native Wayland client, and that is the path with no HDR -- so
|
|
# record them before enumerating anything.
|
|
cat > "$WORK/probe.sh" <<'PROBE'
|
|
#!/usr/bin/env bash
|
|
echo "child_wayland_display=${WAYLAND_DISPLAY:-unset}"
|
|
echo "child_display=${DISPLAY:-unset}"
|
|
echo "child_enable_gamescope_wsi=${ENABLE_GAMESCOPE_WSI:-unset}"
|
|
echo "---VULKANINFO---"
|
|
# Keep stderr. A Vulkan init failure and a missing HDR format both end up as
|
|
# "no formats" otherwise, and only one of them is this script's business.
|
|
if vulkaninfo 2>&1; then
|
|
echo "probe_vulkaninfo=ok"
|
|
else
|
|
echo "probe_vulkaninfo=failed rc=$?"
|
|
fi
|
|
PROBE
|
|
chmod +x "$WORK/probe.sh"
|
|
|
|
echo "enumerating surface formats under nescope…"
|
|
NESCOPE_RC=0
|
|
timeout 60 "$ROOT/target/release/nescope" --hdr "${NESCOPE_ARGS[@]}" \
|
|
--width 1280 --height 720 \
|
|
-- "$WORK/probe.sh" > "$WORK/probe.out" 2>"$WORK/nescope.log" || NESCOPE_RC=$?
|
|
|
|
# A compositor that died, or a probe that never got a Vulkan instance, is an
|
|
# environment failure. Reporting it as missing HDR formats would be the exact
|
|
# confusion this script exists to prevent, so say which one it was and show the
|
|
# log rather than deleting it with the temp dir.
|
|
if [ "$NESCOPE_RC" -eq 124 ]; then
|
|
echo
|
|
echo "FAIL — nescope did not exit within 60s; the run was killed." >&2
|
|
echo "This is an environment failure, not an HDR result. Last log lines:" >&2
|
|
tail -20 "$WORK/nescope.log" >&2
|
|
exit 2
|
|
fi
|
|
|
|
if grep -q "^probe_vulkaninfo=failed" "$WORK/probe.out"; then
|
|
echo
|
|
echo "FAIL — vulkaninfo failed inside the compositor." >&2
|
|
echo "This is an environment failure, not an HDR result:" >&2
|
|
sed -n "/---VULKANINFO---/,/probe_vulkaninfo=failed/p" "$WORK/probe.out" \
|
|
| grep -iE "error|cannot|failed|no such" | head -10 >&2 || true
|
|
exit 2
|
|
fi
|
|
|
|
if ! grep -q "^probe_vulkaninfo=ok" "$WORK/probe.out"; then
|
|
echo
|
|
echo "FAIL — the probe did not run to completion (nescope rc=$NESCOPE_RC)." >&2
|
|
echo "This is an environment failure, not an HDR result. Last log lines:" >&2
|
|
tail -20 "$WORK/nescope.log" >&2
|
|
exit 2
|
|
fi
|
|
|
|
python3 - "$WORK/probe.out" "$MODE" <<'PY'
|
|
import re, sys
|
|
|
|
path, mode = sys.argv[1], sys.argv[2]
|
|
text = open(path, errors="replace").read()
|
|
|
|
env = dict(re.findall(r"^(child_\w+)=(.*)$", text, re.M))
|
|
print()
|
|
print(f"child WAYLAND_DISPLAY: {env.get('child_wayland_display', '?')}")
|
|
print(f"child DISPLAY: {env.get('child_display', '?')}")
|
|
print(f"ENABLE_GAMESCOPE_WSI: {env.get('child_enable_gamescope_wsi', '?')}")
|
|
|
|
# Pull the format lists from the presentable-surface section of the first real
|
|
# GPU. llvmpipe is enumerated too and would double every count, so skip any
|
|
# adapter that names it -- a software rasteriser's opinion about HDR is not the
|
|
# thing under test.
|
|
#
|
|
# Keep the XCB and Wayland surfaces apart. Once the child has a DISPLAY it has
|
|
# both, they carry different formats, and merging them would let one path's HDR
|
|
# support stand in for the other's. The XCB list is the one a Proton game sees.
|
|
# One adapter only, and named in the output. Appending every adapter's formats
|
|
# into one list would let a second GPU satisfy the checks while the one the game
|
|
# runs on lacks the formats entirely -- a pass that means nothing. There is no
|
|
# way to ask vulkaninfo which adapter a game would pick, so this takes the first
|
|
# hardware one and says which, leaving a multi-GPU host to be read rather than
|
|
# guessed at.
|
|
section = text.split("Presentable Surfaces", 1)
|
|
by_path = {"xcb": [], "wayland": []}
|
|
gpu, path, chosen_gpu, other_gpus = None, None, None, []
|
|
if len(section) > 1:
|
|
block, cur_fmt = section[1], None
|
|
for line in block.splitlines():
|
|
m = re.match(r"\s*GPU id\s*:\s*\d+\s*\((.+?)\)\s*\[(.+?)\]", line)
|
|
if m:
|
|
gpu, exts = m.group(1), m.group(2)
|
|
path = "wayland" if "wayland_surface" in exts else "xcb"
|
|
if "llvmpipe" not in gpu:
|
|
if chosen_gpu is None:
|
|
chosen_gpu = gpu
|
|
elif gpu != chosen_gpu and gpu not in other_gpus:
|
|
other_gpus.append(gpu)
|
|
continue
|
|
# Skip the software rasteriser, and every hardware adapter after the
|
|
# first: their formats are not the ones under test.
|
|
if gpu is None or "llvmpipe" in gpu or gpu != chosen_gpu:
|
|
continue
|
|
m = re.match(r"\s*format\s*=\s*(\S+)", line)
|
|
if m:
|
|
cur_fmt = m.group(1)
|
|
continue
|
|
m = re.match(r"\s*colorSpace\s*=\s*(\S+)", line)
|
|
if m and cur_fmt and path:
|
|
# vulkaninfo pads its listing with FORMAT_UNDEFINED entries when a
|
|
# layer appends to the surface format list. Checked against the API
|
|
# directly with the two-call pattern -- the count and the entries
|
|
# agree there, and VK_INCOMPLETE comes back on a short buffer -- so
|
|
# these are an artifact of the listing, not formats a client sees.
|
|
if cur_fmt != "FORMAT_UNDEFINED":
|
|
by_path[path].append((cur_fmt, m.group(1)))
|
|
cur_fmt = None
|
|
|
|
# A game under XWayland presents through the XCB surface, so that is the list
|
|
# under test whenever it exists. Fall back to the Wayland one when the child
|
|
# never got a DISPLAY, which is the only case where it is what a game would use.
|
|
on_xwayland = bool(by_path["xcb"])
|
|
formats = by_path["xcb"] if on_xwayland else by_path["wayland"]
|
|
|
|
fails = []
|
|
if not formats:
|
|
fails.append("no surface formats enumerated at all — the probe never reached "
|
|
"a surface, so this run measured nothing")
|
|
|
|
print(f"\nadapter under test: {chosen_gpu or '(none found)'}")
|
|
if other_gpus:
|
|
print(f" ignoring {len(other_gpus)} other adapter(s): {', '.join(other_gpus)}")
|
|
print(" a game may not pick the one above; check it is the render device")
|
|
print(f"path under test: {'XCB (XWayland)' if on_xwayland else 'native Wayland'}")
|
|
if on_xwayland:
|
|
print(f" (native Wayland surface offers {len(by_path['wayland'])} formats, not under test)")
|
|
print(f"\nsurface formats offered: {len(formats)}")
|
|
for f, cs in formats:
|
|
print(f" {f:<34} {cs}")
|
|
|
|
spaces = {cs for _, cs in formats}
|
|
depth10 = [f for f, _ in formats if "10" in f and "B8G8R8A8" not in f]
|
|
depth16 = [f for f, _ in formats if "16G16" in f or "SFLOAT" in f]
|
|
|
|
print()
|
|
print(f"HDR10_ST2084 offered: {'yes' if any('ST2084' in s for s in spaces) else 'no'}")
|
|
print(f"scRGB linear offered: {'yes' if any('EXTENDED_SRGB_LINEAR' in s for s in spaces) else 'no'}")
|
|
print(f"10-bit formats offered: {'yes' if depth10 else 'no'}")
|
|
print(f"FP16 formats offered: {'yes' if depth16 else 'no'}")
|
|
|
|
if formats:
|
|
if mode == "baseline":
|
|
# Only what is genuinely working today. The point of this mode is to
|
|
# notice if the colour-management path stops being wired up at all,
|
|
# which would otherwise look identical to plain SDR.
|
|
# Guard both halves of what the compositor itself controls: the colour
|
|
# spaces, which come from the colour-management protocol, and the pixel
|
|
# formats, which come from the dmabuf list. Each fails silently on its
|
|
# own -- a missing format is simply absent, with nothing logged.
|
|
wl = by_path["wayland"]
|
|
wl_spaces = {cs for _, cs in wl}
|
|
if not any("ST2084" in s for s in wl_spaces):
|
|
fails.append("HDR10_ST2084_EXT is no longer offered on the Wayland "
|
|
"surface — the wp_color_manager_v1 path has stopped "
|
|
"being advertised")
|
|
if not [f for f, _ in wl if "10" in f and "B8G8R8A8" not in f]:
|
|
fails.append("no 10-bit pixel format on the Wayland surface — Mesa "
|
|
"drops any format lacking either its alpha or its "
|
|
"opaque FourCC spelling, so check both are advertised")
|
|
if not [f for f, _ in wl if "16G16" in f]:
|
|
fails.append("no FP16 pixel format on the Wayland surface — the "
|
|
"scRGB path needs R16G16B16A16_SFLOAT")
|
|
else:
|
|
# The three a WSI layer injects. Until one exists these all fail, and
|
|
# that is the expected reading, not a defect in this script.
|
|
if not any("ST2084" in s for s in spaces):
|
|
fails.append("HDR10_ST2084_EXT not offered")
|
|
if not depth10:
|
|
fails.append("no 10-bit format offered — PQ over 8-bit BGRA bands; a "
|
|
"WSI layer must inject A2B10G10R10/A2R10G10B10")
|
|
if not any("EXTENDED_SRGB_LINEAR" in s for s in spaces):
|
|
fails.append("EXTENDED_SRGB_LINEAR_EXT not offered — Proton titles on "
|
|
"the scRGB path find no matching format and fall back to SDR")
|
|
if not depth16:
|
|
fails.append("no FP16 format offered — the scRGB path needs "
|
|
"R16G16B16A16_SFLOAT")
|
|
|
|
# The display the child inherited decides which of the two code paths it is on,
|
|
# and only one of them can carry HDR today. Worth saying plainly either way,
|
|
# because a native-Wayland client failing the layer checks above is failing for
|
|
# a reason that has nothing to do with formats.
|
|
if env.get("child_display", "unset") == "unset":
|
|
print("\nnote: the child had no DISPLAY and ran as a native Wayland client,")
|
|
print(" which is the intended configuration — HDR is only offered on the")
|
|
print(" Wayland surface. Pass --xwayland to nescope for X11-only software,")
|
|
print(" at the cost of that software's HDR.")
|
|
else:
|
|
print("\nnote: the child had a DISPLAY, so XWayland is running. A game that")
|
|
print(" presents through it gets no HDR: Mesa offers no HDR colour space")
|
|
print(" on the XWayland surface.")
|
|
|
|
print()
|
|
if fails:
|
|
print("FAIL" + (" (expected until the WSI layer lands)" if mode == "layer" else ""))
|
|
for f in fails:
|
|
print(f" - {f}")
|
|
sys.exit(1)
|
|
print("PASS")
|
|
PY
|