Files
netris-nestri/apps/nescapture
Kristian Ollikainen 8246aa5538 feat: resident guest init (#333)
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>
2026-09-14 14:45:13 +03:00
..
2026-09-14 14:45:13 +03:00

nescapture

A Vulkan implicit layer that captures frames from inside the workload's own process, encodes them with Vulkan Video on the GPU that drew them, and sends them to neshub over a Unix socket — no copy out to the CPU and back.

Being a layer rather than a screen-scraper is the whole point: the frame is already on the GPU when we get it, and it never leaves.


Where it sits

Game process
    │  Vulkan calls
    ▼
┌──────────────────────────────────────────────┐
│  nescapture implicit layer                   │
│                                              │
│  vkCreateShaderModule       → SHA-256 hash   │
│  vkCreateGraphicsPipelines  → track hashes   │
│  vkCmdBindPipeline          → detect HUD     │
│  vkQueuePresentKHR          → capture+encode │
└──────────────────────────────────────────────┘
    │  GPU blit, same device
    ▼
final_image  (DMA-BUF exportable)
    │  get_dmabuf_fd(final_memory)
    ▼
DmaBufImporter  (pixelforge VkDevice)
    │  import_or_reuse() → vk::Image
    ▼
ColorConverter  (GPU compute shader)
    │  BGRA/RGB10/FP16 → NV12/P010/YUV444
    ▼
Encoder  (Vulkan Video: H.264 / H.265 / AV1)
    │  Annex-B packets
    ▼
Unix datagram → neshub → the client

CPU fallback exists only for driver configurations without DMA-BUF external memory export.


Sockets

path direction carries
/tmp/nestri-video.sock nescapture → neshub encoded frames
/tmp/nestri-stats.sock nescapture → neshub capture fps, encode ms, drops
/tmp/nescapture-cmd.sock neshub → nescapture IDR requests, encode settings

nescapture binds the command socket and connects to the other two. The stats path is derived from NESCAPTURE_IPC_PATH's directory, so moving the video socket moves both.


Quick start

# 1. Build
cargo build --release -p nescapture

# 2. Install the layer manifest
sudo cp apps/nescapture/manifest/VK_LAYER_nescapture.json /usr/share/vulkan/implicit_layer.d/
# Point the manifest's `library_path` at target/release/libnescapture_layer.so

# 3. Run something that draws
export NESCAPTURE_ENABLE=1
export NESCAPTURE_CODEC=h265
export NESCAPTURE_BITRATE=10000
export RUST_LOG=info

./my-vulkan-app

The layer is inert unless NESCAPTURE_ENABLE=1. That is deliberate — an implicit layer is loaded into every Vulkan process on the system.


Environment variables

Variable Default Description
NESCAPTURE_ENABLE (unset) Set to 1 to activate the layer. Nothing happens otherwise
NESCAPTURE_IPC_PATH /tmp/nestri-video.sock Where to send encoded frames
NESCAPTURE_CODEC best available h264, h265 or av1; probes if unset
NESCAPTURE_FORMAT yuv420 yuv420 or yuv444
NESCAPTURE_DEPTH auto 8 or 10; inferred from the swapchain VkFormat if unset
NESCAPTURE_BITRATE 10000 CBR target in kbps. Ignored when NESCAPTURE_QP is set
NESCAPTURE_QP (unset) Constant QP instead of CBR
NESCAPTURE_FPS 60 Target frame rate
NESCAPTURE_IDR_INTERVAL 4 Force an IDR every N seconds
NESCAPTURE_TUNE (unset) highquality, lowlatency, ultralowlatency, lossless
NESCAPTURE_CONFIG (unset) Path to the per-app shader-hash TOML
NESCAPTURE_GAME_NAME exe basename Override app identification for that config
NESCAPTURE_DISCOVER (unset) Set to 1 to log every draw, for finding HUD shaders
RUST_LOG error Standard env_logger filter, e.g. nescapture_layer=debug

Everything else is decided at runtime: the client asks neshub for a codec or bitrate change and it arrives on the command socket, so the encoder is reconfigured without a restart.


Per-app shader-hash config

HUD detection needs to know which pipelines draw the HUD, and that is per-application. Run once with NESCAPTURE_DISCOVER=1 to log every shader, then pick out the HUD pipelines by the [SUSPECT] marker — blend on, depth off, six vertices or fewer.

# ~/.config/nescapture/apps.toml
[game."MyApp.exe"]
hud_fragment_shaders  = ["0xaabbccddeeff0011"]
hud_vertex_shaders    = ["0x1a2b3c4d5e6f7890"]
skip_fragment_shaders = ["0x1122334455667788"]
export NESCAPTURE_CONFIG=~/.config/nescapture/apps.toml
export NESCAPTURE_GAME_NAME=MyApp.exe

Modules

src/
├── lib.rs           entry points, dispatch routing
├── dispatch.rs      Vulkan function-pointer types and tables
├── state.rs         global DashMaps, DeviceState, CbState
├── instance.rs      vkCreateInstance / vkDestroyInstance
├── device.rs        vkCreateDevice / vkDestroyDevice
├── shader.rs        SPIR-V hashing
├── pipeline.rs      graphics pipeline tracking
├── framebuffer.rs   image view and framebuffer tracking
├── commands.rs      vkCmdBind*, vkCmdDraw*, vkCmdBeginRenderPass
├── swapchain.rs     vkCreateSwapchainKHR, image enumeration
├── capture.rs       GPU blit to the capture image, DMA-BUF export
├── present.rs       vkQueuePresentKHR, encode dispatch
├── encode.rs        pixelforge pipeline, codec probing, IPC send
├── dmabuf_import.rs cross-device zero-copy import
├── config.rs        per-app TOML shader-hash config
└── discovery.rs     draw-call logging for shader discovery

Dependencies

Crate Purpose
pixelforge Vulkan Video hardware encode
ash Vulkan bindings
nesprotocol The IPC frame format neshub reads
sha2, bytemuck SPIR-V fingerprinting
dashmap, once_cell Lock-free concurrent state
serde, toml Per-app shader config

ash and pixelforge are pinned to git revisions — both track Vulkan Video support that has not landed in a release.


Licence

Apache 2.0. See LICENSE.