mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
Get this thing going..
<!-- greptile_comment -->
<!-- greptile_summary -->
<h2><a
href="https://app.greptile.com/api/retrigger?id=63134761"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/RetriggerDark.svg?v=1"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=1"><img
alt="Retrigger"
src="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=1"
align="right"></picture></a>Confidence Score: 5/5</h2>
The PR appears safe to merge; all previous findings are resolved and the
latest readiness change introduces no established actionable regression.
<h3>Summary</h3>
- Establishes required guest filesystems, runtime directories, device
permissions, and service processes.
- Reports initialization and service deaths over the lifecycle channel.
- Supports launch, restart, and shutdown commands for a resident guest.
- Separates service and workload identities and configures per-launch
runtime environments.
- Removes the currently inactive nescope screenshot option and makes
capture-chain verification fail explicitly when compositor readback is
unavailable.
- Reworks the guest image around `nesinit` as PID 1 without a
distribution service manager.
<h3>Diagram</h3>
```mermaid
sequenceDiagram
participant Host
participant Init as nesinit
participant FS as Guest filesystems
participant Services as Service stack
participant Workload
Init->>Host: Ready(protocol version)
Host->>Init: Boot(mount descriptors)
Init->>FS: Establish and mount shares
Init->>Services: Spawn services in order
Services-->>Init: Required sockets ready
Init->>Host: Initialized(service names)
Host->>Init: Launch(id, exec, on_exit)
Init->>Workload: Spawn with isolated UID/runtime
Init->>Host: Started(id)
Workload-->>Init: Exit status
Init->>Host: WorkloadExited(id, status)
Host->>Init: Launch / Restart / Shutdown
```
<sub>Reviews (4) · Last reviewed commit: ["fix(nesinit): readiness is a
socket
that..."](731d34df9d)</sub>
<!-- /greptile_comment -->
---------
Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
202 lines
8.4 KiB
Bash
Executable File
202 lines
8.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Verify the capture chain end to end on this machine's GPU.
|
|
#
|
|
# Runs a Vulkan workload under the compositor with the layer active, then checks
|
|
# the encoded result against the compositor's own readback of the same frames.
|
|
# Two independent paths see the same content: the compositor reads the surface
|
|
# back to the CPU, the layer exports it as a DMA-BUF and encodes it on the GPU.
|
|
# Agreement between them is the evidence; a single path cannot tell a correct
|
|
# frame from a plausible-looking wrong one.
|
|
#
|
|
# The failure this is really aimed at is silent: a black or mis-levelled frame
|
|
# arrives as a valid stream at the right frame rate, and every liveness check
|
|
# passes. So the checks below are about pixel values, not about whether bytes
|
|
# moved.
|
|
#
|
|
# This covers the SDR path only. The HDR arms need a swapchain this workload
|
|
# cannot ask for, and the check that matters there is a different one — absolute
|
|
# sample values against the standard, rather than two instruments against each
|
|
# other. See verify-hdr.sh.
|
|
#
|
|
# Usage: apps/nescapture/scripts/verify-chain.sh [seconds]
|
|
set -euo pipefail
|
|
|
|
SECS="${1:-16}"
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
|
WORK="$(mktemp -d)"
|
|
trap 'rm -rf "$WORK"; kill $(jobs -p) 2>/dev/null || true' EXIT
|
|
|
|
: "${XDG_RUNTIME_DIR:=/run/user/$(id -u)}"
|
|
export XDG_RUNTIME_DIR
|
|
|
|
for tool in ffmpeg ffprobe vkcube python3; do
|
|
command -v "$tool" >/dev/null || { echo "missing required tool: $tool" >&2; exit 1; }
|
|
done
|
|
|
|
echo "building…"
|
|
cargo build --release -p nescope -p nescapture --manifest-path "$ROOT/Cargo.toml" >/dev/null
|
|
|
|
# The whole method here is two independent instruments on the same frames, and
|
|
# the second one is the compositor's own readback. While nescope's screenshot
|
|
# path is commented out there is no second instrument, so this script cannot
|
|
# make the comparison it exists for. Said here rather than fifty lines later as
|
|
# "compositor readback produced no frames", which reads like a capture bug.
|
|
#
|
|
# Asked of the binary rather than hard-coded, so this comes back by itself on
|
|
# the commit that brings the path back.
|
|
if ! "$ROOT/target/release/nescope" --help 2>&1 | grep -q -- --screenshot-ipc; then
|
|
echo "this nescope has no --screenshot-ipc, so there is no readback to compare" >&2
|
|
echo "the encoded frames against; the GPU readback path in nescope is" >&2
|
|
echo "commented out. See apps/nescope/src/main.rs." >&2
|
|
exit 1
|
|
fi
|
|
|
|
LAYER="$ROOT/target/release/libnescapture_layer.so"
|
|
MANIFEST_DIR="$WORK/implicit_layer.d"
|
|
mkdir -p "$MANIFEST_DIR"
|
|
sed "s#\"library_path\": \".*\"#\"library_path\": \"$LAYER\"#" \
|
|
"$ROOT/apps/nescapture/manifest/VK_LAYER_nescapture.json" > "$MANIFEST_DIR/VK_LAYER_nescapture.json"
|
|
export VK_ADD_IMPLICIT_LAYER_PATH="$MANIFEST_DIR"
|
|
|
|
VIDEO_SOCK="$WORK/video.sock"
|
|
SHOT_SOCK="$WORK/shot.sock"
|
|
STREAM="$WORK/capture.h264"
|
|
|
|
cat > "$WORK/recv.py" <<'PY'
|
|
import os, socket, struct, sys, time
|
|
sock, out, secs = sys.argv[1], sys.argv[2], float(sys.argv[3])
|
|
s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 8 << 20)
|
|
s.bind(sock); os.chmod(sock, 0o777); s.settimeout(1.0)
|
|
n = 0; end = time.time() + secs
|
|
with open(out, "wb") as f:
|
|
while time.time() < end:
|
|
try: buf = s.recv(8 << 20)
|
|
except socket.timeout: continue
|
|
if len(buf) < 20 or buf[:4] != b"NSTR" or buf[4] != 0: continue
|
|
(_, _, _, dl) = struct.unpack("<IHHI", buf[8:20])
|
|
f.write(buf[20:20 + dl]); n += 1
|
|
print(n)
|
|
PY
|
|
|
|
echo "capturing for ${SECS}s…"
|
|
"$ROOT/target/release/nescope-shot" --socket "$SHOT_SOCK" --watch --interval 1000 \
|
|
--keep 3 --out "$WORK/shot.ppm" >/dev/null 2>&1 &
|
|
python3 "$WORK/recv.py" "$VIDEO_SOCK" "$STREAM" "$SECS" > "$WORK/frames.txt" &
|
|
RECV=$!
|
|
sleep 1
|
|
|
|
NESCAPTURE_ENABLE=1 NESCAPTURE_CODEC=h264 NESCAPTURE_BITRATE=20000 NESCAPTURE_FPS=60 \
|
|
NESCAPTURE_IPC_PATH="$VIDEO_SOCK" RUST_LOG=nescapture_layer=debug \
|
|
timeout "$((SECS - 2))" "$ROOT/target/release/nescope" \
|
|
--width 1280 --height 720 --fps 60 --screenshot-ipc "$SHOT_SOCK" \
|
|
-- vkcube --c 100000 > "$WORK/run.log" 2>&1 || true
|
|
wait $RECV || true
|
|
|
|
FRAMES="$(cat "$WORK/frames.txt")"
|
|
echo
|
|
echo "frames encoded: $FRAMES"
|
|
grep -m1 "First import" "$WORK/run.log" || echo " (no DMA-BUF import logged)"
|
|
|
|
python3 - "$WORK" "$STREAM" "$FRAMES" <<'PY'
|
|
import glob, subprocess, sys
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
work, stream, frames = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
|
fails = []
|
|
|
|
if frames < 30:
|
|
fails.append(f"only {frames} frames encoded (want >= 30)")
|
|
|
|
probe = subprocess.run(
|
|
["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
|
|
"stream=color_range", "-of", "default=noprint_wrappers=1:nokey=1", stream],
|
|
capture_output=True, text=True).stdout.strip()
|
|
print(f"declared range: {probe or '(none)'}")
|
|
if probe != "pc":
|
|
fails.append(f"stream declares color_range={probe or 'unset'}; the converter "
|
|
"writes full-range samples, so the tag must be 'pc'")
|
|
|
|
subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", stream, "-vf",
|
|
r"select='eq(n\,120)+eq(n\,240)'", "-fps_mode", "passthrough",
|
|
f"{work}/dec_%02d.png"], check=True)
|
|
|
|
def luma(a):
|
|
return 0.2126 * a[..., 0] + 0.7152 * a[..., 1] + 0.0722 * a[..., 2]
|
|
|
|
dec = [np.asarray(Image.open(p).convert("RGB")).astype(float)
|
|
for p in sorted(glob.glob(f"{work}/dec_*.png"))]
|
|
shots = [np.asarray(Image.open(p).convert("RGB")).astype(float)
|
|
for p in sorted(glob.glob(f"{work}/shot-*.ppm"))]
|
|
|
|
if not dec:
|
|
fails.append("nothing decoded from the stream")
|
|
if not shots:
|
|
fails.append("compositor readback produced no frames")
|
|
|
|
if dec:
|
|
worst = min(luma(d).std() for d in dec)
|
|
print(f"decoded luma std: {worst:.2f}")
|
|
# An absolute floor only has to catch a blank frame, which sits near zero.
|
|
# How much structure a *correct* frame carries depends entirely on what the
|
|
# workload drew, so the real check is the relative one below, against the
|
|
# readback of the same content.
|
|
if worst < 5.0:
|
|
fails.append(f"decoded frames are near-uniform (luma std {worst:.2f}) — "
|
|
"the classic silent failure is a blank frame at full frame rate")
|
|
|
|
if dec and shots:
|
|
ds, ss = min(luma(d).std() for d in dec), min(luma(s).std() for s in shots)
|
|
print(f"readback luma std:{ss:.2f}")
|
|
if ss > 1.0 and abs(ds - ss) / ss > 0.25:
|
|
fails.append(f"decoded structure {ds:.2f} vs readback {ss:.2f} — the two "
|
|
"paths saw the same frames, so they should carry the same detail")
|
|
|
|
if dec and shots:
|
|
# Compare a corner, not the whole frame. The two instruments sample at
|
|
# different moments -- the readback is on a 1 s timer, the decoded frames are
|
|
# picked by index -- so any whole-frame statistic also carries whatever the
|
|
# workload was doing at each instant. The workload draws a centred object on
|
|
# a flat background, so a corner patch is the same colour in every frame and
|
|
# the comparison stops depending on lining them up.
|
|
#
|
|
# This is the measurement that catches a range or matrix error: a flat patch
|
|
# of known colour, decoded, against the same patch read back from the
|
|
# compositor. It is where a full-range/limited-range mismatch shows up as a
|
|
# constant offset.
|
|
def corner(a):
|
|
return a[8:72, 8:72]
|
|
|
|
def spread(patches):
|
|
m = [luma(p).mean() for p in patches]
|
|
return max(m) - min(m)
|
|
|
|
dc, sc = [corner(d) for d in dec], [corner(s) for s in shots]
|
|
a = float(np.mean([luma(p).mean() for p in sc]))
|
|
b = float(np.mean([luma(p).mean() for p in dc]))
|
|
print(f"readback corner: {a:.2f}")
|
|
print(f"decoded corner: {b:.2f}")
|
|
print(f"difference: {abs(a - b):.2f}")
|
|
|
|
# If the corner is not actually flat across frames, the assumption above does
|
|
# not hold for this workload and the comparison would be measuring animation.
|
|
# Say so rather than reporting a number that means nothing.
|
|
drift = max(spread(dc), spread(sc))
|
|
if drift > 3.0:
|
|
fails.append(f"the corner patch varies by {drift:.2f} between frames, so it "
|
|
"is not background here; the brightness check assumes a "
|
|
"workload that leaves its corners alone")
|
|
elif abs(a - b) > 4.0:
|
|
fails.append(f"the two paths disagree on brightness by {abs(a-b):.2f}; "
|
|
"they are looking at the same content, so one of them is wrong")
|
|
|
|
print()
|
|
if fails:
|
|
print("FAIL")
|
|
for f in fails:
|
|
print(f" - {f}")
|
|
sys.exit(1)
|
|
print("PASS")
|
|
PY
|