mirror of
https://dev.lirent.ru/Vatrog/vm-automation-signaling.git
synced 2026-06-25 20:36:36 +03:00
Compare commits
15 Commits
v0.3.0
...
bcf5d4f824
| Author | SHA1 | Date | |
|---|---|---|---|
|
bcf5d4f824
|
|||
|
7ab6119b1f
|
|||
|
0f452fe37c
|
|||
|
85041c12ab
|
|||
|
228dc5af79
|
|||
|
d6c45ddb04
|
|||
|
929bcf0e74
|
|||
|
3142337e62
|
|||
|
26e5ab4709
|
|||
|
6fea392d46
|
|||
|
0289817821
|
|||
|
d1aa09ecac
|
|||
|
64b1a39b55
|
|||
|
b14ea0e381
|
|||
|
bd8b966017
|
@@ -0,0 +1,134 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
# No deployment-specific values are hardcoded: server/owner/repo come from the CI context,
|
||||
# the publish token from a secret. Mirrors the sibling vmie release pipeline.
|
||||
jobs:
|
||||
# In-guest vgpu producer (Windows, cross-compiled) -> attached to the release.
|
||||
windows-agent:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:20-bookworm-slim
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install toolchain
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
cmake make zip jq curl ca-certificates gcc-mingw-w64-x86-64
|
||||
|
||||
- name: Cross-build the agent
|
||||
run: |
|
||||
cmake -S . -B build-win -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-mingw-w64.cmake
|
||||
cmake --build build-win -j
|
||||
|
||||
- name: Package
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p dist/vgpu-streamer
|
||||
cp build-win/vgpu-streamer.exe dist/vgpu-streamer/
|
||||
[ -f LICENSE ] && cp LICENSE dist/vgpu-streamer/ || true
|
||||
(cd dist && zip -r "vgpu-streamer-${TAG}-win64.zip" vgpu-streamer)
|
||||
|
||||
- name: Attach to release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
|
||||
SERVER: https://dev.lirent.ru
|
||||
REPO: ${{ github.repository }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
asset="vgpu-streamer-${TAG}-win64.zip"
|
||||
api="${SERVER}/api/v1/repos/${REPO}"
|
||||
auth="Authorization: token ${GITEA_TOKEN}"
|
||||
|
||||
rid=$(curl -sSL -H "$auth" "${api}/releases/tags/${TAG}" | jq -r '.id // empty' || true)
|
||||
if [ -z "$rid" ]; then
|
||||
rid=$(curl -fsSL -X POST -H "$auth" -H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\"}" \
|
||||
"${api}/releases" | jq -r '.id')
|
||||
fi
|
||||
curl -fsSL -H "$auth" "${api}/releases/${rid}/assets" \
|
||||
| jq -r ".[] | select(.name==\"${asset}\") | .id" \
|
||||
| while read -r aid; do
|
||||
[ -n "$aid" ] && curl -fsSL -X DELETE -H "$auth" "${api}/releases/${rid}/assets/${aid}"
|
||||
done
|
||||
curl -fsSL -X POST -H "$auth" \
|
||||
-F "attachment=@dist/${asset};type=application/zip" \
|
||||
"${api}/releases/${rid}/assets?name=${asset}"
|
||||
|
||||
# Host package (daemon + libs) -> the Gitea Debian registry. Built against the published
|
||||
# vmie dev package (external dependency), installed from the same registry.
|
||||
deb:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:20-bookworm-slim
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install toolchain + vmie (external dependency)
|
||||
env:
|
||||
SERVER: https://dev.lirent.ru
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
ACTOR: ${{ github.actor }}
|
||||
TOKEN: ${{ secrets.PUBLISH_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
cmake make gcc libc6-dev dpkg-dev file ca-certificates curl
|
||||
# The Gitea Debian registry is private: apt needs HTTP Basic Auth. [trusted=yes]
|
||||
# only skips GPG verification, NOT authentication — hence the prior 401. The token
|
||||
# is written to auth.conf.d (never echoed to the log).
|
||||
# machine MUST carry the scheme (https://) — apt refuses to send credentials over an
|
||||
# unencrypted/unannotated endpoint (the prior 401 over the plain-http internal IP).
|
||||
install -d -m 0700 /etc/apt/auth.conf.d
|
||||
printf 'machine %s login %s password %s\n' "$SERVER" "$ACTOR" "$TOKEN" \
|
||||
> /etc/apt/auth.conf.d/gitea.conf
|
||||
chmod 600 /etc/apt/auth.conf.d/gitea.conf
|
||||
echo "deb [trusted=yes] ${SERVER}/api/packages/${OWNER}/debian stable main" \
|
||||
> /etc/apt/sources.list.d/gitea.list
|
||||
apt-get update
|
||||
apt-get install -y libvmie-dev
|
||||
|
||||
- name: Build package
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: make deb VERSION="${TAG#v}"
|
||||
|
||||
- name: Publish to Debian registry
|
||||
env:
|
||||
TOKEN: ${{ secrets.PUBLISH_TOKEN }} # requires scope: package:write
|
||||
SERVER: https://dev.lirent.ru
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
DISTRIBUTION: stable
|
||||
COMPONENT: main
|
||||
run: |
|
||||
set -euo pipefail
|
||||
url="${SERVER}/api/packages/${OWNER}/debian/pool/${DISTRIBUTION}/${COMPONENT}/upload"
|
||||
auth="Authorization: token ${TOKEN}"
|
||||
for deb in dist/*.deb; do
|
||||
# 201 Created = uploaded; 409 Conflict = this version already present (re-run).
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -X PUT -H "$auth" -T "$deb" "$url")
|
||||
echo "$deb -> HTTP $code"
|
||||
if [ "$code" != 201 ] && [ "$code" != 409 ]; then
|
||||
echo "upload failed: $deb (HTTP $code)" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
@@ -4,3 +4,4 @@ compile*
|
||||
Testing/
|
||||
CLAUDE.md
|
||||
dist/
|
||||
!.gitea/
|
||||
|
||||
+92
-24
@@ -1,5 +1,8 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(vmsig VERSION 0.3.0 LANGUAGES C)
|
||||
# Single source of truth for the version: CI passes -DVMSIG_VERSION=${TAG#v}, so the project
|
||||
# version (-> libvgpu-perception SONAME/.so version) and the .deb version come from one tag.
|
||||
set(VMSIG_VERSION "0.3.12" CACHE STRING "Release version (MAJOR.MINOR.PATCH); CI passes the tag")
|
||||
project(vmsig VERSION ${VMSIG_VERSION} LANGUAGES C)
|
||||
|
||||
set(CMAKE_C_STANDARD 17)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
@@ -16,6 +19,37 @@ option(VMSIG_WITH_VMIE "Link real vmie (libvmie.a, PIC) for armed memctx" OFF)
|
||||
# The input driver (vmctl) is ABSORBED in-tree (src/si/input/) — no external flag.
|
||||
set(LIBVMIE_PATH "" CACHE PATH "Path to the vmie library sources (for VMSIG_WITH_VMIE)")
|
||||
|
||||
# ---- in-guest vgpu producer (Windows agent, cross-compiled) -----------------
|
||||
# The host signaling stack below is Linux-only (epoll/eventfd/timerfd), so a Windows-targeted
|
||||
# build (mingw toolchain, CMAKE_SYSTEM_NAME=Windows) produces ONLY this agent. Producer and
|
||||
# host consumer share the ABI header include/vgpu_stream.h, so they version together in one tree.
|
||||
# cmake -S . -B .build-win -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-mingw-w64.cmake
|
||||
if(WIN32)
|
||||
add_executable(vgpu-streamer
|
||||
src/si/vgpu-stream/win32/main.c
|
||||
src/si/vgpu-stream/publish.c
|
||||
src/si/vgpu-stream/win32/region.c
|
||||
src/si/vgpu-stream/win32/present.c
|
||||
src/si/vgpu-stream/win32/cursor.c
|
||||
src/si/vgpu-stream/win32/geometry.c
|
||||
src/si/vgpu-stream/win32/capture.c
|
||||
src/si/vgpu-stream/win32/capture_nvfbc.c
|
||||
src/si/vgpu-stream/win32/capture_dda.c
|
||||
src/si/vgpu-stream/win32/capture_gdi.c)
|
||||
target_include_directories(vgpu-streamer PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/si/vgpu-stream/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/si/vgpu-stream/win32
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party) # vendor NvFBC + Windows.h shim
|
||||
target_compile_definitions(vgpu-streamer PRIVATE CINTERFACE WIN32_LEAN_AND_MEAN=)
|
||||
target_compile_options(vgpu-streamer PRIVATE
|
||||
$<$<C_COMPILER_ID:GNU>:-O2;-Wall;-Wextra>
|
||||
$<$<C_COMPILER_ID:MSVC>:/O2;/W3>)
|
||||
target_link_libraries(vgpu-streamer PRIVATE d3d11 dxgi dxguid uuid user32 gdi32)
|
||||
target_link_options(vgpu-streamer PRIVATE $<$<C_COMPILER_ID:GNU>:-static;-s>)
|
||||
return() # a Windows-targeted build is the agent ONLY; the host stack below is skipped
|
||||
endif()
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
# ---- signaling library ------------------------------------------------------
|
||||
@@ -31,6 +65,7 @@ add_library(vmsig SHARED
|
||||
src/control/socket.c
|
||||
src/discovery/slot.c
|
||||
src/discovery/linux/host_probe.c
|
||||
src/discovery/linux/mtree.c
|
||||
src/discovery/discovery.c
|
||||
# SI input driver (vmctl), absorbed in-tree (host-only: QMP + uinput)
|
||||
src/si/input/open.c
|
||||
@@ -58,9 +93,19 @@ target_link_libraries(vmsig PRIVATE Threads::Threads)
|
||||
# package Depends on libvmie). Headers + symbols come from the imported target.
|
||||
if(VMSIG_WITH_VMIE)
|
||||
add_library(vmie SHARED IMPORTED)
|
||||
set_target_properties(vmie PROPERTIES
|
||||
IMPORTED_LOCATION ${LIBVMIE_PATH}/.build/libvmie.so
|
||||
INTERFACE_INCLUDE_DIRECTORIES ${LIBVMIE_PATH}/include)
|
||||
if(LIBVMIE_PATH)
|
||||
# dev: link against an in-place source-tree build
|
||||
set_target_properties(vmie PROPERTIES
|
||||
IMPORTED_LOCATION ${LIBVMIE_PATH}/.build/libvmie.so
|
||||
INTERFACE_INCLUDE_DIRECTORIES ${LIBVMIE_PATH}/include)
|
||||
else()
|
||||
# CI/system: the installed libvmie-dev package (/usr, or via CMAKE_PREFIX_PATH)
|
||||
find_library(VMIE_LIBRARY NAMES vmie REQUIRED)
|
||||
find_path( VMIE_INCLUDE_DIR NAMES memmodel.h PATH_SUFFIXES vmie REQUIRED)
|
||||
set_target_properties(vmie PROPERTIES
|
||||
IMPORTED_LOCATION ${VMIE_LIBRARY}
|
||||
INTERFACE_INCLUDE_DIRECTORIES ${VMIE_INCLUDE_DIR})
|
||||
endif()
|
||||
target_link_libraries(vmsig PRIVATE vmie)
|
||||
target_compile_definitions(vmsig PRIVATE VMSIG_WITH_VMIE)
|
||||
endif()
|
||||
@@ -76,16 +121,18 @@ add_executable(vmsig_cli src/cli.c)
|
||||
target_link_libraries(vmsig_cli PRIVATE vmsig)
|
||||
target_compile_options(vmsig_cli PRIVATE -Wall -Wextra)
|
||||
|
||||
# ---- vgpu-perception: host-side vgpu Sensor S-lib (absorbed in-tree) ---------
|
||||
# A SEPARATE shipped library (NOT fused into libvmsig — it is consumed by the shell, not the
|
||||
# signaling core). Host-only: reads the vgpu shared region from its own RO vmie_mem. Built
|
||||
# only when armed (needs vmie). The in-guest Windows producer (vgpu-streamer.exe) stays in a
|
||||
# separate repo and is NOT part of this delivery.
|
||||
# ---- vgpu-perception: host-side vgpu Sensor S-lib ---------------------------
|
||||
# Packaged SEPARATELY from the daemon (libvgpu-perception0 + -dev), NOT fused into libvmsig —
|
||||
# a Sensor lib consumed by a control/shell, not the signaling core. Host-only: reads the vgpu
|
||||
# shared region from its own RO vmie_mem. Built only when armed (needs vmie). The in-guest
|
||||
# Windows producer is the vgpu-streamer cross-target above (same tree, shared ABI vgpu_stream.h).
|
||||
if(VMSIG_WITH_VMIE)
|
||||
add_library(vgpu-perception SHARED
|
||||
src/si/vgpu-perception/discover.c
|
||||
src/si/vgpu-perception/sample.c
|
||||
src/si/vgpu-perception/control.c)
|
||||
set_target_properties(vgpu-perception PROPERTIES
|
||||
VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR}) # libvgpu-perception.so.0
|
||||
target_include_directories(vgpu-perception
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/si/vgpu-perception/include)
|
||||
@@ -170,6 +217,15 @@ target_include_directories(vmsig_discoverytest PRIVATE
|
||||
target_compile_options(vmsig_discoverytest PRIVATE -Wall -Wextra)
|
||||
add_test(NAME discovery COMMAND vmsig_discoverytest)
|
||||
|
||||
add_executable(vmsig_mtreetest src/test/test_mtree.c)
|
||||
target_link_libraries(vmsig_mtreetest PRIVATE vmsig)
|
||||
target_include_directories(vmsig_mtreetest PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/discovery/include)
|
||||
target_compile_definitions(vmsig_mtreetest PRIVATE
|
||||
FIXTURE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/src/test/fixtures")
|
||||
target_compile_options(vmsig_mtreetest PRIVATE -Wall -Wextra)
|
||||
add_test(NAME mtree COMMAND vmsig_mtreetest)
|
||||
|
||||
add_executable(vmsig_daemoncfgtest
|
||||
src/test/test_daemoncfg.c
|
||||
src/daemon/config.c
|
||||
@@ -213,6 +269,13 @@ target_link_libraries(vmsig_inputobstest PRIVATE vmsig Threads::Threads)
|
||||
target_compile_options(vmsig_inputobstest PRIVATE -Wall -Wextra)
|
||||
add_test(NAME inputobs COMMAND vmsig_inputobstest)
|
||||
|
||||
add_executable(vmsig_uinputlayouttest src/test/test_uinputlayout.c)
|
||||
target_link_libraries(vmsig_uinputlayouttest PRIVATE vmsig)
|
||||
target_include_directories(vmsig_uinputlayouttest PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/si/input/include)
|
||||
target_compile_options(vmsig_uinputlayouttest PRIVATE -Wall -Wextra)
|
||||
add_test(NAME uinputlayout COMMAND vmsig_uinputlayouttest)
|
||||
|
||||
add_executable(vmsig_memwritetest src/test/test_memwrite.c)
|
||||
target_link_libraries(vmsig_memwritetest PRIVATE vmsig Threads::Threads)
|
||||
target_include_directories(vmsig_memwritetest PRIVATE
|
||||
@@ -224,25 +287,30 @@ add_test(NAME memwrite COMMAND vmsig_memwritetest)
|
||||
add_test(NAME cli COMMAND vmsig_cli)
|
||||
|
||||
# ---- install rules (for the .deb stage) -------------------------------------
|
||||
option(VMSIG_INSTALL "Generate install() rules (daemon/lib/headers/unit/config)" OFF)
|
||||
option(VMSIG_INSTALL "Generate install() rules (per-component, for the .deb stages)" OFF)
|
||||
if(VMSIG_INSTALL)
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS vmsigd RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
|
||||
install(TARGETS vmsig LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
if(TARGET vgpu-perception) # armed builds ship the host vgpu S-lib alongside
|
||||
install(TARGETS vgpu-perception LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
endif()
|
||||
# public contracts (signaling + absorbed SI host headers) under include/vmsig/
|
||||
# --- component `daemon`: the signaling delivery (package: vmsig). NO gpu lib here. ---
|
||||
install(TARGETS vmsigd RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR} COMPONENT daemon)
|
||||
install(TARGETS vmsig LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT daemon)
|
||||
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/vmsig
|
||||
FILES_MATCHING PATTERN "vmsig*.h"
|
||||
PATTERN "vmctl.h"
|
||||
PATTERN "vgpu_stream.h"
|
||||
PATTERN "vgpu_perception.h")
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/vmsig COMPONENT daemon
|
||||
FILES_MATCHING PATTERN "vmsig*.h" PATTERN "vmctl.h")
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/systemd/vmsigd.service
|
||||
DESTINATION lib/systemd/system)
|
||||
DESTINATION lib/systemd/system COMPONENT daemon)
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/tmpfiles/vmsig.conf
|
||||
DESTINATION lib/tmpfiles.d)
|
||||
DESTINATION lib/tmpfiles.d COMPONENT daemon)
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/config/vmsigd.conf
|
||||
DESTINATION /etc/vmsig)
|
||||
DESTINATION /etc/vmsig COMPONENT daemon)
|
||||
# --- the host vgpu perception S-lib, SEPARATE from the daemon: runtime (versioned .so,
|
||||
# package libvgpu-perception0) vs dev (namelink + headers, package libvgpu-perception-dev) ---
|
||||
if(TARGET vgpu-perception)
|
||||
install(TARGETS vgpu-perception
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
COMPONENT vgpu_runtime
|
||||
NAMELINK_COMPONENT vgpu_dev)
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/include/vgpu_perception.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/vgpu_stream.h
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/vmsig COMPONENT vgpu_dev)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -1,46 +1,60 @@
|
||||
# vmsig packaging — `make deb` builds the .deb over a `cmake --install` stage.
|
||||
# Private values are NOT baked into the tree: pass them via the variables below (the
|
||||
# defaults are neutral placeholders; CI overrides them from vars/secrets).
|
||||
# vmsig packaging — `make deb` builds TWO Debian packages from per-component install stages:
|
||||
# vmsig — the signaling daemon + library + headers + systemd unit
|
||||
# libvgpu-perception — the host-side vgpu perception S-lib (SEPARATE: not in vmsig)
|
||||
# Private values are NOT baked in: pass them via the variables below (CI overrides them).
|
||||
#
|
||||
# make deb LIBVMIE_PATH=/path/to/vmie VERSION=1.2.3 \
|
||||
# MAINTAINER="Name <addr>" DEPENDS="libc6, libvmie0"
|
||||
# make deb LIBVMIE_PATH=/path/to/vmie VERSION=1.2.3 MAINTAINER="Name <addr>"
|
||||
|
||||
VERSION ?= 0.0.0
|
||||
MAINTAINER ?= vmsig packaging <root@localhost>
|
||||
# libvmie0 is vmie's own runtime package (SONAME libvmie.so.0): libvmsig.so and
|
||||
# libvgpu-perception.so dynamically link it, so it is a HARD runtime dependency.
|
||||
DEPENDS ?= libc6, libvmie0
|
||||
ARCH ?= amd64
|
||||
VERSION ?= 0.0.0
|
||||
MAINTAINER ?= vmsig packaging <root@localhost>
|
||||
# libvmie0 is vmie's runtime package (SONAME libvmie.so.0): both libvmsig.so and
|
||||
# libvgpu-perception.so dynamically link it — a hard runtime dependency of each package.
|
||||
DEPENDS ?= libc6, libvmie0
|
||||
ARCH ?= amd64
|
||||
LIBVMIE_PATH ?=
|
||||
|
||||
BUILD_DIR ?= .build-pkg
|
||||
STAGE ?= $(CURDIR)/dist/stage
|
||||
DIST ?= $(CURDIR)/dist
|
||||
BUILD_DIR ?= .build-pkg
|
||||
DIST ?= $(CURDIR)/dist
|
||||
|
||||
.PHONY: deb clean
|
||||
|
||||
# Armed package: the shipped daemon needs vmie for memctx. vmie stays an external dependency
|
||||
# (the package Depends on its runtime; pass DEPENDS to add it).
|
||||
# vmie is found from a source tree (LIBVMIE_PATH) or, when empty, the installed libvmie-dev
|
||||
# (system / CMAKE_PREFIX_PATH) — the CI path.
|
||||
deb:
|
||||
@test -n "$(LIBVMIE_PATH)" || { echo "set LIBVMIE_PATH=/path/to/vmie sources (armed memctx)"; exit 1; }
|
||||
rm -rf $(STAGE)
|
||||
cmake -S . -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Release -DVMSIG_INSTALL=ON \
|
||||
-DVMSIG_WITH_VMIE=ON -DLIBVMIE_PATH=$(LIBVMIE_PATH)
|
||||
cmake -S . -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Release -DVMSIG_INSTALL=ON -DVMSIG_WITH_VMIE=ON \
|
||||
-DVMSIG_VERSION=$(VERSION) $(if $(LIBVMIE_PATH),-DLIBVMIE_PATH=$(LIBVMIE_PATH),)
|
||||
cmake --build $(BUILD_DIR) -j
|
||||
DESTDIR=$(STAGE) cmake --install $(BUILD_DIR) --prefix /usr
|
||||
mkdir -p $(STAGE)/DEBIAN
|
||||
sed -e 's/@VERSION@/$(VERSION)/' \
|
||||
-e 's|@MAINTAINER@|$(MAINTAINER)|' \
|
||||
-e 's/@DEPENDS@/$(DEPENDS)/' \
|
||||
packaging/deb/control.in > $(STAGE)/DEBIAN/control
|
||||
cp packaging/deb/conffiles $(STAGE)/DEBIAN/conffiles
|
||||
install -m 0755 packaging/deb/postinst $(STAGE)/DEBIAN/postinst
|
||||
install -m 0755 packaging/deb/prerm $(STAGE)/DEBIAN/prerm
|
||||
# strip inherited setgid from staged dirs (a setgid build tree => dpkg-deb rejects DEBIAN)
|
||||
find $(STAGE) -type d -exec chmod g-s {} +
|
||||
mkdir -p $(DIST)
|
||||
dpkg-deb --root-owner-group --build $(STAGE) $(DIST)/vmsig_$(VERSION)_$(ARCH).deb
|
||||
@echo "built: $(DIST)/vmsig_$(VERSION)_$(ARCH).deb"
|
||||
# ---- package: vmsig (component `daemon`) ----
|
||||
rm -rf $(DIST)/stage-daemon
|
||||
DESTDIR=$(DIST)/stage-daemon cmake --install $(BUILD_DIR) --prefix /usr --component daemon
|
||||
mkdir -p $(DIST)/stage-daemon/DEBIAN
|
||||
sed -e 's/@VERSION@/$(VERSION)/' -e 's|@MAINTAINER@|$(MAINTAINER)|' -e 's/@DEPENDS@/$(DEPENDS)/' \
|
||||
packaging/deb/vmsig/control.in > $(DIST)/stage-daemon/DEBIAN/control
|
||||
cp packaging/deb/vmsig/conffiles $(DIST)/stage-daemon/DEBIAN/conffiles
|
||||
install -m 0755 packaging/deb/vmsig/postinst $(DIST)/stage-daemon/DEBIAN/postinst
|
||||
install -m 0755 packaging/deb/vmsig/prerm $(DIST)/stage-daemon/DEBIAN/prerm
|
||||
find $(DIST)/stage-daemon -type d -exec chmod g-s {} +
|
||||
dpkg-deb --root-owner-group --build $(DIST)/stage-daemon $(DIST)/vmsig_$(VERSION)_$(ARCH).deb
|
||||
# ---- package: libvgpu-perception0 (component `vgpu_runtime` — versioned .so) ----
|
||||
rm -rf $(DIST)/stage-vgpu0
|
||||
DESTDIR=$(DIST)/stage-vgpu0 cmake --install $(BUILD_DIR) --prefix /usr --component vgpu_runtime
|
||||
mkdir -p $(DIST)/stage-vgpu0/DEBIAN
|
||||
sed -e 's/@VERSION@/$(VERSION)/' -e 's|@MAINTAINER@|$(MAINTAINER)|' -e 's/@DEPENDS@/$(DEPENDS)/' \
|
||||
packaging/deb/vgpu0/control.in > $(DIST)/stage-vgpu0/DEBIAN/control
|
||||
install -m 0755 packaging/deb/vgpu0/postinst $(DIST)/stage-vgpu0/DEBIAN/postinst
|
||||
find $(DIST)/stage-vgpu0 -type d -exec chmod g-s {} +
|
||||
dpkg-deb --root-owner-group --build $(DIST)/stage-vgpu0 $(DIST)/libvgpu-perception0_$(VERSION)_$(ARCH).deb
|
||||
# ---- package: libvgpu-perception-dev (component `vgpu_dev` — namelink + headers) ----
|
||||
rm -rf $(DIST)/stage-vgpu-dev
|
||||
DESTDIR=$(DIST)/stage-vgpu-dev cmake --install $(BUILD_DIR) --prefix /usr --component vgpu_dev
|
||||
mkdir -p $(DIST)/stage-vgpu-dev/DEBIAN
|
||||
sed -e 's/@VERSION@/$(VERSION)/' -e 's|@MAINTAINER@|$(MAINTAINER)|' \
|
||||
-e 's/@DEPENDS@/libvgpu-perception0 (= $(VERSION))/' \
|
||||
packaging/deb/vgpu-dev/control.in > $(DIST)/stage-vgpu-dev/DEBIAN/control
|
||||
find $(DIST)/stage-vgpu-dev -type d -exec chmod g-s {} +
|
||||
dpkg-deb --root-owner-group --build $(DIST)/stage-vgpu-dev $(DIST)/libvgpu-perception-dev_$(VERSION)_$(ARCH).deb
|
||||
@echo "built: vmsig + libvgpu-perception0 + libvgpu-perception-dev ($(VERSION))"
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR) $(DIST)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
set(CMAKE_SYSTEM_NAME Windows)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
|
||||
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
+8
-9
@@ -15,10 +15,6 @@ typedef enum {
|
||||
/* via QEMU virtio-input-host-pci (Linux). uinput != virtio. */
|
||||
} vmctl_driver;
|
||||
|
||||
#define VMCTL_PTR_ABS 1 /* uinput: absolute tablet */
|
||||
#define VMCTL_PTR_REL 2 /* uinput: relative mouse */
|
||||
#define VMCTL_PTR_BOTH 3 /* uinput: two devices A=abs B=rel */
|
||||
|
||||
typedef struct {
|
||||
unsigned bustype; /* HID bus type, e.g. 0x0003 (USB) */
|
||||
unsigned vendor; /* vendor id */
|
||||
@@ -31,15 +27,20 @@ typedef struct {
|
||||
vmctl_driver driver;
|
||||
const char* qmp_path; /* QMP unix socket; required for QMP, optional (passthrough) for UINPUT */
|
||||
const char* input_bus; /* virtio-input-host-pci bus "pci.0" for passthrough; "" = none */
|
||||
int ptr_mode; /* UINPUT VMCTL_PTR_*; 0 for QMP */
|
||||
const vmctl_uinput_id* uinput_id; /* UINPUT only; NULL = built-in defaults */
|
||||
} vmctl_config;
|
||||
|
||||
vmctl_t* vmctl_open (const vmctl_config* cfg); /* NULL on error */
|
||||
void vmctl_close(vmctl_t* v); /* safe on NULL */
|
||||
|
||||
/* Copy the host evdev node paths of the created uinput devices (UINPUT driver only).
|
||||
* a[] receives device A (keyboard), b[] receives device B (relative mouse); both are always
|
||||
* created, so count==2 in the normal case. Each buffer must be >=64 bytes. Returns the count
|
||||
* of non-empty paths filled (0/1/2), or -1 if the handle's driver is not UINPUT. Paths are
|
||||
* valid while the handle is open. */
|
||||
int vmctl_uinput_evdev(vmctl_t* v, char a[64], char b[64]);
|
||||
|
||||
/* ===== Input constants ===== */
|
||||
#define VMCTL_ABS_MAX 32767 /* abs coordinates 0..VMCTL_ABS_MAX */
|
||||
#define VMCTL_AXIS_X 0
|
||||
#define VMCTL_AXIS_Y 1
|
||||
#define VMCTL_SCROLL_V 0 /* vertical */
|
||||
@@ -61,13 +62,12 @@ void vmctl_close(vmctl_t* v); /* safe on NULL */
|
||||
typedef struct {
|
||||
int kind; /* internal event-kind code; set by builders */
|
||||
int code; /* axis / button / evdev-code (per kind) */
|
||||
int value; /* abs-value / rel-delta / down(0|1) */
|
||||
int value; /* rel-delta / down(0|1) */
|
||||
double scroll; /* scroll magnitude (scroll only) */
|
||||
} vmctl_event;
|
||||
typedef struct { vmctl_event ev[VMCTL_BATCH_MAX]; int count; } vmctl_batch;
|
||||
|
||||
void vmctl_batch_init (vmctl_batch* b);
|
||||
void vmctl_batch_abs (vmctl_batch* b, int axis, int value);
|
||||
void vmctl_batch_rel (vmctl_batch* b, int axis, int delta);
|
||||
void vmctl_batch_btn (vmctl_batch* b, int btn, int down);
|
||||
void vmctl_batch_key (vmctl_batch* b, int evdev_code, int down);
|
||||
@@ -75,7 +75,6 @@ void vmctl_batch_scroll(vmctl_batch* b, int axis, double value);
|
||||
int vmctl_batch_send (vmctl_t* v, vmctl_batch* b); /* one round-trip; 0=ok, -1=err */
|
||||
|
||||
/* ===== Single events (wrappers over a 1-event batch) ===== */
|
||||
int vmctl_abs (vmctl_t* v, int axis, int value); /* 0..VMCTL_ABS_MAX */
|
||||
int vmctl_rel (vmctl_t* v, int axis, int delta);
|
||||
int vmctl_btn (vmctl_t* v, int btn, int down); /* VMCTL_BTN_* */
|
||||
int vmctl_key (vmctl_t* v, int evdev_code, int down); /* Linux KEY_* */
|
||||
|
||||
@@ -159,9 +159,12 @@ enum {
|
||||
* encodes vmsig_input into vmsig_event.inln.
|
||||
*
|
||||
* Pointer motion carries BOTH coordinates in ONE event (a pointer position is a single entity,
|
||||
* not two independent axis updates). btn/key/scroll stay single-valued. */
|
||||
* not two independent axis updates). btn/key/scroll stay single-valued.
|
||||
*
|
||||
* Numbering is FROZEN: an external control encodes these on the wire and is not rebuilt from
|
||||
* this header. Removing a member must NOT shift the others. */
|
||||
typedef enum {
|
||||
VMSIG_INPUT_MOVE_ABS = 0, /* absolute pointer: x,y are coordinates (0..VMCTL_ABS_MAX) */
|
||||
/* 0 reserved (was MOVE_ABS, removed) */
|
||||
VMSIG_INPUT_MOVE_REL = 1, /* relative pointer: x,y are deltas (dx,dy) */
|
||||
VMSIG_INPUT_BTN = 2, /* button: code=button, value=pressed(1)/released(0) */
|
||||
VMSIG_INPUT_KEY = 3, /* key: code=evdev code, value=pressed/released */
|
||||
@@ -175,8 +178,8 @@ typedef struct {
|
||||
uint16_t kind; /* vmsig_input_kind */
|
||||
uint16_t code; /* button / evdev code / scroll axis (NOT used by MOVE_*) */
|
||||
int32_t value; /* pressed(1)|released(0) for BTN/KEY (not used by MOVE or SCROLL) */
|
||||
int32_t x; /* MOVE_ABS: abs X (0..VMCTL_ABS_MAX); MOVE_REL: dx */
|
||||
int32_t y; /* MOVE_ABS: abs Y; MOVE_REL: dy */
|
||||
int32_t x; /* MOVE_REL: dx */
|
||||
int32_t y; /* MOVE_REL: dy */
|
||||
double scroll; /* SCROLL magnitude only */
|
||||
uint32_t flags; /* VMSIG_INPUT_F_* (see above) */
|
||||
uint32_t _pad; /* reserved; zero on emit */
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
case "$1" in
|
||||
configure)
|
||||
ldconfig || true
|
||||
mkdir -p /etc/vmsig
|
||||
chmod 0640 /etc/vmsig/vmsigd.conf 2>/dev/null || true # carries the uid->grant policy
|
||||
mkdir -p /dev/shm/vmsig && chmod 0755 /dev/shm/vmsig # also (re)created at boot via tmpfiles
|
||||
if [ -d /run/systemd/system ]; then
|
||||
systemctl daemon-reload || true
|
||||
systemd-tmpfiles --create /usr/lib/tmpfiles.d/vmsig.conf || true
|
||||
systemctl enable vmsigd.service || true # enable, but do NOT start
|
||||
fi
|
||||
echo "vmsig: review the [grant] policy in /etc/vmsig/vmsigd.conf, then: systemctl start vmsigd" >&2
|
||||
;;
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
@@ -0,0 +1,10 @@
|
||||
Package: libvgpu-perception-dev
|
||||
Version: @VERSION@
|
||||
Section: libdevel
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Depends: @DEPENDS@
|
||||
Maintainer: @MAINTAINER@
|
||||
Description: Host-side vgpu perception library (development files)
|
||||
Headers (vgpu_perception.h, vgpu_stream.h) and the linker namelink for
|
||||
libvgpu-perception. Install this to build a control/shell against the perception API.
|
||||
@@ -0,0 +1,12 @@
|
||||
Package: libvgpu-perception0
|
||||
Version: @VERSION@
|
||||
Section: libs
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Depends: @DEPENDS@
|
||||
Maintainer: @MAINTAINER@
|
||||
Description: Host-side vgpu perception library
|
||||
Reads the in-guest vgpu shared region (frames, cursor, geometry) from the host over a
|
||||
read-only guest-RAM handle and exposes a perception API. A Sensor-layer library consumed
|
||||
by a control/shell, independent of the signaling daemon. This package ships the runtime
|
||||
shared object (libvgpu-perception.so.0).
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
case "$1" in
|
||||
configure)
|
||||
ldconfig || true
|
||||
;;
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
@@ -5,9 +5,9 @@ Priority: optional
|
||||
Architecture: amd64
|
||||
Depends: @DEPENDS@
|
||||
Maintainer: @MAINTAINER@
|
||||
Description: VM signaling coherence daemon and host SI libraries
|
||||
Description: VM signaling coherence daemon
|
||||
vmsig serves a unix-socket control plane over the signaling layer for the VMs it
|
||||
discovers: lifecycle/state, coherent guest address-space context handoff, and arbitrated
|
||||
input and memory-write actuation. Ships the daemon (vmsigd), the signaling library, the
|
||||
host-side vgpu perception library, and a systemd unit. Configured via
|
||||
/etc/vmsig/vmsigd.conf.
|
||||
input and memory-write actuation. Ships the daemon (vmsigd), the signaling library, and a
|
||||
systemd unit. Configured via /etc/vmsig/vmsigd.conf. The host-side vgpu perception library
|
||||
is a separate package (libvgpu-perception).
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
case "$1" in
|
||||
configure)
|
||||
ldconfig || true
|
||||
mkdir -p /etc/vmsig
|
||||
chmod 0640 /etc/vmsig/vmsigd.conf 2>/dev/null || true # carries the uid->grant policy
|
||||
mkdir -p /dev/shm/vmsig && chmod 0755 /dev/shm/vmsig # also (re)created at boot via tmpfiles
|
||||
if [ -d /run/systemd/system ]; then
|
||||
systemctl daemon-reload || true
|
||||
systemd-tmpfiles --create /usr/lib/tmpfiles.d/vmsig.conf || true
|
||||
systemctl enable vmsigd.service || true # enable, but do NOT start
|
||||
fi
|
||||
if [ -z "$2" ]; then
|
||||
# fresh install ($2 empty): enabled but NOT started — the operator reviews the
|
||||
# grant policy before the first start.
|
||||
echo "vmsig: review the [grant] policy in /etc/vmsig/vmsigd.conf, then: systemctl start vmsigd" >&2
|
||||
else
|
||||
# upgrade ($2 = old version): a running daemon keeps the OLD in-memory image until
|
||||
# restarted — the new build is not applied automatically. Not auto-restarted here:
|
||||
# the start is gated on the grant policy, so the operator owns the moment. try-restart
|
||||
# touches the daemon only if it is currently running (leaves a stopped one alone).
|
||||
echo "vmsig: upgraded from $2 — a running daemon still runs the old build; apply with: systemctl try-restart vmsigd" >&2
|
||||
fi
|
||||
;;
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
@@ -3,11 +3,18 @@
|
||||
|
||||
/* Private config of the input adapter (vmctl, in-tree at src/si/input/). cfg==NULL or
|
||||
* stub!=0 => stub mode (ack without actuation). stub==0 opens vmctl_open() and actuates for
|
||||
* real. Injection is ALWAYS uinput (orphaned host uinput + external QEMU input-linux);
|
||||
* qmp_path is kept for the SERVICE path (power/lifecycle via vmctl QMP), not for injection. */
|
||||
* real. Injection is ALWAYS uinput; the created evdev nodes are forwarded into the guest by an
|
||||
* input-linux QMP object that the vmhost seam adds over its own connection (this adapter only
|
||||
* publishes the evdev paths, it never touches QMP). qmp_path is kept for the SERVICE path
|
||||
* (power/lifecycle via vmctl QMP), not for injection. */
|
||||
typedef struct {
|
||||
int stub;
|
||||
const char* qmp_path; /* for power/lifecycle (vmctl QMP); NOT input injection */
|
||||
/* On a real attach the adapter writes the uinput evdev node paths here (>=64 bytes each)
|
||||
* so the vmhost seam can bridge them via input-linux. NULL => not published; B is "" when
|
||||
* there is no second device. Buffers belong to the caller and outlive the adapter. */
|
||||
char* out_evdev_a;
|
||||
char* out_evdev_b;
|
||||
} vmsig_input_cfg;
|
||||
|
||||
/* Input event codes/contract are PUBLIC: vmsig_input / vmsig_input_kind in
|
||||
|
||||
+24
-13
@@ -22,8 +22,8 @@ typedef struct {
|
||||
int kind; /* vmsig_input_kind (for cmd==0) */
|
||||
int code; /* btn/evdev-code/scroll-axis */
|
||||
int value; /* pressed(1)/released(0) for btn/key */
|
||||
int x; /* MOVE_ABS: abs X; MOVE_REL: dx */
|
||||
int y; /* MOVE_ABS: abs Y; MOVE_REL: dy */
|
||||
int x; /* MOVE_REL: dx */
|
||||
int y; /* MOVE_REL: dy */
|
||||
double scroll;
|
||||
int noack; /* CMD_INPUT fire-and-forget: emit no ACT_ACK */
|
||||
int life_op; /* VMSIG_LIFE_* (powerdown/reset/wakeup/pause/resume) */
|
||||
@@ -39,6 +39,8 @@ struct vmsig_adapter {
|
||||
vmsig_worker* worker;
|
||||
const char* qmp_path; /* borrowed from cfg (valid through attach); SERVICE power/lifecycle */
|
||||
vmctl_t* vmctl; /* NULL in stub mode (cfg.stub) — no actuator opened */
|
||||
char* out_evdev_a; /* borrowed home for the uinput evdev path of A; NULL = not published */
|
||||
char* out_evdev_b; /* likewise for B; "" written when there is no second device */
|
||||
};
|
||||
|
||||
static int input_job(void* user, const void* reqp, void* resp) {
|
||||
@@ -55,10 +57,6 @@ static int input_job(void* user, const void* reqp, void* resp) {
|
||||
/* Pointer motion is ONE packet: both axes in a single batch -> one round-trip. */
|
||||
vmctl_batch b; vmctl_batch_init(&b);
|
||||
switch (rq->kind) {
|
||||
case VMSIG_INPUT_MOVE_ABS:
|
||||
vmctl_batch_abs(&b, VMCTL_AXIS_X, rq->x);
|
||||
vmctl_batch_abs(&b, VMCTL_AXIS_Y, rq->y);
|
||||
break;
|
||||
case VMSIG_INPUT_MOVE_REL:
|
||||
vmctl_batch_rel(&b, VMCTL_AXIS_X, rq->x);
|
||||
vmctl_batch_rel(&b, VMCTL_AXIS_Y, rq->y);
|
||||
@@ -66,7 +64,7 @@ static int input_job(void* user, const void* reqp, void* resp) {
|
||||
case VMSIG_INPUT_BTN: vmctl_batch_btn(&b, rq->code, rq->value); break;
|
||||
case VMSIG_INPUT_KEY: vmctl_batch_key(&b, rq->code, rq->value); break;
|
||||
case VMSIG_INPUT_SCROLL: vmctl_batch_scroll(&b, rq->code, rq->scroll); break;
|
||||
default: break;
|
||||
default: break; /* unknown/0 kind (e.g. retired MOVE_ABS): no-op */
|
||||
}
|
||||
r = vmctl_batch_send(a->vmctl, &b);
|
||||
} else {
|
||||
@@ -93,7 +91,11 @@ static vmsig_adapter* in_open(const void* cfg, uint32_t endpoint) {
|
||||
if (!a) return NULL;
|
||||
a->endpoint = endpoint;
|
||||
a->stub = c ? c->stub : 1;
|
||||
if (c) a->qmp_path = c->qmp_path; /* carry to attach (cfg not passed there); SERVICE power */
|
||||
if (c) {
|
||||
a->qmp_path = c->qmp_path; /* carry to attach (cfg not passed there); SERVICE power */
|
||||
a->out_evdev_a = c->out_evdev_a;
|
||||
a->out_evdev_b = c->out_evdev_b;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
@@ -104,19 +106,28 @@ static int in_attach(vmsig_adapter* a, const vmsig_emit* emit, vmsig_fd_reg* reg
|
||||
if (!a->worker) return -1;
|
||||
|
||||
if (!a->stub) {
|
||||
/* armed: open the actuator. Injection is ALWAYS uinput (orphaned host uinput + external
|
||||
* QEMU input-linux). PTR_BOTH gives both pointer forms a device (A=abs tablet, B=rel
|
||||
* mouse) — the contract now promises both MOVE_ABS and MOVE_REL, so neither may be
|
||||
* disabled. qmp_path serves the SERVICE power/lifecycle path, not input injection. */
|
||||
/* armed: open the actuator. Injection is ALWAYS uinput; the resulting evdev nodes are
|
||||
* forwarded into the guest by the vmhost seam's input-linux object (published below).
|
||||
* uinput always creates two devices: A=keyboard, B=relative mouse+buttons+wheel — the
|
||||
* contract carries MOVE_REL (there is no absolute pointer). qmp_path serves the SERVICE
|
||||
* power/lifecycle path, not input injection. */
|
||||
vmctl_config vcfg;
|
||||
memset(&vcfg, 0, sizeof vcfg);
|
||||
vcfg.driver = VMCTL_DRIVER_UINPUT;
|
||||
vcfg.qmp_path = a->qmp_path;
|
||||
vcfg.input_bus = "";
|
||||
vcfg.ptr_mode = VMCTL_PTR_BOTH;
|
||||
vcfg.uinput_id = NULL; /* built-in HID identity defaults */
|
||||
a->vmctl = vmctl_open(&vcfg);
|
||||
if (!a->vmctl) { vmsig_worker_free(a->worker); a->worker = NULL; return -1; }
|
||||
|
||||
/* Publish the created evdev node paths into the stable per-endpoint home so the vmhost
|
||||
* seam can bridge them via input-linux. The seam reads these after both attaches. */
|
||||
if (a->out_evdev_a || a->out_evdev_b) {
|
||||
char ea[64] = {0}, eb[64] = {0};
|
||||
vmctl_uinput_evdev(a->vmctl, ea, eb);
|
||||
if (a->out_evdev_a) memcpy(a->out_evdev_a, ea, sizeof ea);
|
||||
if (a->out_evdev_b) memcpy(a->out_evdev_b, eb, sizeof eb);
|
||||
}
|
||||
}
|
||||
|
||||
reg[0].fd = vmsig_worker_evfd(a->worker);
|
||||
|
||||
@@ -12,6 +12,16 @@ typedef struct {
|
||||
/* TRANSFERS to the adapter (closed in close()) — the */
|
||||
/* caller dups first if it must keep its own copy. */
|
||||
/* <0 => default: open(ram_path, O_RDONLY) / stub-memfd */
|
||||
uint32_t fail_boots; /* test-only: fail the first N stub bootstraps before */
|
||||
/* succeeding (drives the retry/backoff path deterministically */
|
||||
/* without timing dependence); 0 in production. stub path only. */
|
||||
const char* persist_path; /* armed: path to the kcr3 cache file (sibling of .slots in the */
|
||||
/* watch dir, tmpfs-local: survives a daemon restart, dies with the */
|
||||
/* RAM file on host reboot). NULL/empty => persist disabled (cold */
|
||||
/* bootstrap only). The boot-session discriminator is the kcr3 */
|
||||
/* itself: on resume it is validated against live RAM via */
|
||||
/* vmie_win32_open_ro_fd (NULL if it no longer resolves the kernel) */
|
||||
/* — a stale kcr3 after a guest reboot is rejected, fail-closed. */
|
||||
} vmsig_memctx_cfg;
|
||||
|
||||
/* Max SRC bytes per atomic gva_write (bounds the worker POD slot; mc_req header + src
|
||||
|
||||
+331
-39
@@ -25,6 +25,8 @@
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/epoll.h>
|
||||
#include <sys/timerfd.h> /* one-shot backoff timer for cold-bootstrap retry */
|
||||
#include <sys/stat.h> /* persist file mode bits (0600) */
|
||||
|
||||
#ifdef VMSIG_WITH_VMIE
|
||||
#include "win32.h" /* vmie_win32_open/host_bootstrap/proc_list/close */
|
||||
@@ -54,7 +56,94 @@ static int memfd_create(const char* name, unsigned int flags) {
|
||||
#define MC_MAX_SEG 8
|
||||
#define MC_WORKER_DEPTH 16 /* one off-loop thread: rare bootstrap + writes */
|
||||
|
||||
enum { MC_JOB_BOOTSTRAP = 0, MC_JOB_WRITE = 1 };
|
||||
/* Cold-bootstrap retry backoff (guest may still be booting when discovery attaches us;
|
||||
* host_bootstrap then finds no System process). Mirror of the discovery backoff so the
|
||||
* adapter stays decoupled from the discovery layer (Rule-of-three not reached): 50ms base,
|
||||
* exponential with the shift capped at 6, ceiling 2s steady-state. One-shot timerfd: armed
|
||||
* on failure, disarmed on success — no it_interval, no busy-wait. */
|
||||
#define MC_BOOT_BACKOFF_BASE 50000000ull /* 50 ms */
|
||||
#define MC_BOOT_BACKOFF_CAP 2000000000ull /* 2 s */
|
||||
|
||||
/* Adapter readiness fds are demuxed by per-slot cookie: slot 0 is the worker completion
|
||||
* eventfd, slot 1 is the one-shot backoff timerfd that re-kicks the bootstrap. */
|
||||
enum { MC_COOKIE_WORKER = 0, MC_COOKIE_RETRY = 1 };
|
||||
|
||||
/* MC_JOB_RESUME: fast-path boot-session re-validation. On a daemon restart the cold scan
|
||||
* (host_bootstrap) is slow AND unstable (it hunts the agent beacon across physical RAM); if
|
||||
* the guest did NOT reboot, its System DTB (kcr3) is unchanged and was cached at the last
|
||||
* live scan. RESUME re-opens an O_RDONLY context with that cached kcr3 (vmie_win32_open_ro_fd,
|
||||
* which bypasses the beacon scan) — the boot-session discriminator is the kcr3 ITSELF against
|
||||
* the live RAM: it resolves the kernel (ntoskrnl) only if the guest is the same boot. */
|
||||
enum { MC_JOB_BOOTSTRAP = 0, MC_JOB_WRITE = 1, MC_JOB_RESUME = 2 };
|
||||
|
||||
/* ---- kcr3 context persist: a cache of the cold-bootstrap result, mirror of the .slots
|
||||
* idiom in src/discovery/slot.c (magic+version POD, native byte order, atomic tmp+rename,
|
||||
* fail-soft load). Deliberately NOT factored into a shared helper: discovery (vmid<->slot)
|
||||
* and this adapter (kcr3 cache) are different layers with different lifecycles — Rule-of-three
|
||||
* is not reached, and a shared helper would couple the two prematurely.
|
||||
*
|
||||
* We persist the MINIMUM: only {magic, version, kcr3}. NO RAM metadata (st_ino/size/mtime/
|
||||
* btime): those do NOT prove the RAM holds the same boot session (the backing file outlives a
|
||||
* memory overwrite, the inode can be reused). The boot-session discriminator is the kcr3
|
||||
* self-validating against the live RAM at load time (see MC_JOB_RESUME), not file metadata.
|
||||
*
|
||||
* MEMWRITE-target safety: a persisted kcr3 is a READ locator only. The write target (a->kcr3)
|
||||
* is set ONLY by the bootstrap worker after a fresh live scan — never from this file. */
|
||||
#define MC_PERSIST_MAGIC 0x4B435258u /* "KCRX" — kcr3 context cache */
|
||||
#define MC_PERSIST_VERSION 1u
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
uint64_t kcr3; /* System DTB obtained from a live RAM scan; validated by open_ro_fd */
|
||||
} mc_persist_blob;
|
||||
|
||||
/* Atomic save: write a temp sibling then rename over the target, so a reader (or a racing
|
||||
* second daemon) sees either the whole old file or the whole new one. Loop-thread-only.
|
||||
* Returns 0 on success, -1 otherwise (best-effort: the datum is already published). */
|
||||
static int mc_persist_save(const char* path, uint64_t kcr3) {
|
||||
if (!path || !*path) return -1;
|
||||
mc_persist_blob b;
|
||||
memset(&b, 0, sizeof b);
|
||||
b.magic = MC_PERSIST_MAGIC; b.version = MC_PERSIST_VERSION; b.kcr3 = kcr3;
|
||||
|
||||
char tmp[512];
|
||||
int n = snprintf(tmp, sizeof tmp, "%s.tmp", path);
|
||||
if (n < 0 || (size_t)n >= sizeof tmp) return -1;
|
||||
|
||||
int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600);
|
||||
if (fd < 0) return -1;
|
||||
ssize_t w = write(fd, &b, sizeof b);
|
||||
int rc = (w == (ssize_t)sizeof b) ? 0 : -1;
|
||||
if (close(fd) != 0) rc = -1;
|
||||
if (rc == 0 && rename(tmp, path) != 0) rc = -1;
|
||||
if (rc != 0) unlink(tmp);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Load + validate the POD header. Loop-thread-only. Returns 1 if a well-formed blob was read
|
||||
* (out filled), 0 otherwise (no file / short / wrong magic or version => fail-soft, fall back
|
||||
* to a cold bootstrap). No migrations: an old version is ignored and overwritten by the next
|
||||
* live scan result. NOTE: this validates only the file SHAPE; the kcr3 itself is validated
|
||||
* against live RAM on the worker (MC_JOB_RESUME), which is the real boot-session discriminator. */
|
||||
static int mc_persist_load(const char* path, mc_persist_blob* out) {
|
||||
if (!path || !*path) return 0;
|
||||
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) return 0; /* no file => cold bootstrap */
|
||||
mc_persist_blob b;
|
||||
ssize_t r = read(fd, &b, sizeof b);
|
||||
close(fd);
|
||||
if (r != (ssize_t)sizeof b || b.magic != MC_PERSIST_MAGIC || b.version != MC_PERSIST_VERSION)
|
||||
return 0; /* corrupt/old => cold bootstrap */
|
||||
*out = b;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Drop the cache on a destructive VM-lifecycle (the RAM may have changed). Best-effort.
|
||||
* Hygiene only: even without the drop a stale kcr3 would be rejected by the self-validation,
|
||||
* but we do not leave a known-dead file around. Loop-thread-only. */
|
||||
static void mc_persist_drop(const char* path) {
|
||||
if (path && *path) unlink(path);
|
||||
}
|
||||
|
||||
/* worker req/res (POD <= VMSIG_WORK_SLOT). One off-loop worker runs BOTH the cold
|
||||
* bootstrap and the atomic writes (FIFO serializes a write against the close-on-rebootstrap).
|
||||
@@ -63,9 +152,13 @@ enum { MC_JOB_BOOTSTRAP = 0, MC_JOB_WRITE = 1 };
|
||||
* target cr3 (0 => System DTB; resolved on the worker against a->kcr3). */
|
||||
typedef struct {
|
||||
uint32_t op; /* MC_JOB_* */
|
||||
uint32_t boot_count; /* MC_JOB_BOOTSTRAP */
|
||||
/* --- MC_JOB_WRITE --- */
|
||||
uint64_t cr3; /* target AS root; 0 => a->kcr3 (kernel AS), resolved on worker */
|
||||
uint32_t boot_count; /* MC_JOB_BOOTSTRAP: drives the stub kcr3 per epoch */
|
||||
uint32_t attempt; /* MC_JOB_BOOTSTRAP: consecutive-failure index of THIS */
|
||||
/* kick (copy of a->boot_attempts); stub fails while */
|
||||
/* attempt < a->fail_boots. NOT the epoch counter. */
|
||||
/* --- MC_JOB_WRITE / MC_JOB_RESUME --- */
|
||||
uint64_t cr3; /* WRITE: target AS root (0 => a->kcr3); RESUME: persisted kcr3 to validate */
|
||||
uint64_t low; /* MC_JOB_RESUME: below-4G split for vmie_win32_open_ro_fd (ignored by others) */
|
||||
uint64_t gva;
|
||||
uint32_t len;
|
||||
uint32_t corr;
|
||||
@@ -84,12 +177,18 @@ struct vmsig_adapter {
|
||||
uint32_t endpoint;
|
||||
int stub;
|
||||
const char* ram_path; /* armed: RAM-backing path (NOT published outward) */
|
||||
const char* persist_path; /* armed: kcr3 cache file path (cfg, loop-thread-only); NULL => persist off */
|
||||
uint64_t low;
|
||||
int cfg_ro_fd; /* >=0 => infra-sealed RO-fd (owned by adapter, closed in mc_close); <0 => default */
|
||||
vmsig_emit emit;
|
||||
int registered; /* register_memctx already called */
|
||||
vmsig_worker* worker; /* off-loop bootstrap + atomic writes */
|
||||
uint32_t boot_count; /* incremented on each (re-)bootstrap */
|
||||
uint32_t boot_count; /* incremented on each (re-)bootstrap (epoch tag) */
|
||||
|
||||
/* cold-bootstrap retry — loop-thread-only (attach/on_ready/invalidate/close). */
|
||||
int retry_fd; /* one-shot backoff timerfd (-1 when none) */
|
||||
uint32_t boot_attempts; /* consecutive bootstrap failures this cycle (0 = none); reset on success/epoch */
|
||||
uint32_t fail_boots; /* test-only: fail the first N stub bootstraps (cfg); set once in mc_open, then read-only (worker reads it) */
|
||||
|
||||
#ifdef VMSIG_WITH_VMIE
|
||||
vmie_win32* win; /* held RW handle across the epoch (kcr3 source + gva_write target) */
|
||||
@@ -109,6 +208,35 @@ struct vmsig_adapter {
|
||||
/* fwd: MEMWRITE completion ACK (defined below mc_submit; used in mc_on_ready demux). */
|
||||
static void mc_memwrite_ack(struct vmsig_adapter* a, int ok, uint32_t corr, uint32_t origin);
|
||||
|
||||
/* mirror of the discovery backoff; kept in this adapter to stay decoupled from the discovery
|
||||
* layer (Rule-of-three not reached). Exponential with a shift capped at 6, clamped to CAP. */
|
||||
static uint64_t mc_boot_backoff(uint32_t attempts) {
|
||||
uint64_t b = MC_BOOT_BACKOFF_BASE << (attempts < 6 ? attempts : 6);
|
||||
return b > MC_BOOT_BACKOFF_CAP ? MC_BOOT_BACKOFF_CAP : b;
|
||||
}
|
||||
|
||||
/* Arm the one-shot backoff timer (it_value only — no it_interval). Loop-thread-only.
|
||||
* Best-effort: a settime failure is logged, not fatal (matches discovery rearm). */
|
||||
static void mc_arm_retry(struct vmsig_adapter* a) {
|
||||
if (a->retry_fd < 0) return;
|
||||
uint64_t dt = mc_boot_backoff(a->boot_attempts);
|
||||
struct itimerspec its;
|
||||
memset(&its, 0, sizeof its);
|
||||
its.it_value.tv_sec = (time_t)(dt / 1000000000ull);
|
||||
its.it_value.tv_nsec = (long)(dt % 1000000000ull);
|
||||
if (timerfd_settime(a->retry_fd, 0, &its, NULL) != 0)
|
||||
fprintf(stderr, "vmsig memctx: endpoint %u retry timer arm failed\n", a->endpoint);
|
||||
}
|
||||
|
||||
/* Disarm the backoff timer (zero itimerspec). Loop-thread-only. Used on bootstrap success
|
||||
* and at epoch change so a stale arm from a prior failure cannot fire over a fresh cycle. */
|
||||
static void mc_disarm_retry(struct vmsig_adapter* a) {
|
||||
if (a->retry_fd < 0) return;
|
||||
struct itimerspec its;
|
||||
memset(&its, 0, sizeof its);
|
||||
(void)timerfd_settime(a->retry_fd, 0, &its, NULL);
|
||||
}
|
||||
|
||||
/* ---- stub RO-fd: memfd + deterministic contents + seal of future writes ---- */
|
||||
static int mc_make_stub_fd(uint32_t size) {
|
||||
int fd = memfd_create("vmsig_memctx", MFD_CLOEXEC | MFD_ALLOW_SEALING);
|
||||
@@ -182,8 +310,54 @@ static int mc_job(void* user, const void* req, void* res) {
|
||||
#endif
|
||||
}
|
||||
|
||||
if (rq->op == MC_JOB_RESUME) {
|
||||
/* Fast-path boot-session re-validation: open an O_RDONLY context with the PERSISTED
|
||||
* kcr3 and let the engine decide if it still resolves the kernel in the LIVE RAM.
|
||||
* This is purely a READ validation — it NEVER touches a->win/a->mem/a->kcr3 (the
|
||||
* RW write-hold, owned by the bootstrap worker after a fresh live scan). MEMWRITE-
|
||||
* target safety: a persisted kcr3 must never become the gva_write target. */
|
||||
if (a->stub) {
|
||||
/* No VMIE here, so there is no real RAM to validate against: synthetically ACCEPT a
|
||||
* nonzero kcr3 so the stub can exercise the persist MECHANICS (save/load/fast-vs-slow
|
||||
* selection). This is NOT real boot-session validation — that is armed-only. */
|
||||
if (rq->cr3 == 0) return -1;
|
||||
rs->kcr3 = rq->cr3;
|
||||
return 0;
|
||||
}
|
||||
#ifdef VMSIG_WITH_VMIE
|
||||
/* fresh O_RDONLY fd over the backing (same source as mc_reg_share_fd: dup the infra
|
||||
* RO-fd, else open ram_path O_RDONLY). The RO context borrows it (dup'd internally),
|
||||
* so we close our copy after open. */
|
||||
int rfd;
|
||||
if (a->cfg_ro_fd >= 0) rfd = fcntl(a->cfg_ro_fd, F_DUPFD_CLOEXEC, 0);
|
||||
else if (a->ram_path) rfd = open(a->ram_path, O_RDONLY | O_CLOEXEC);
|
||||
else return -1;
|
||||
if (rfd < 0) return -1;
|
||||
vmie_win32* v = vmie_win32_open_ro_fd(rfd, rq->low, rq->cr3);
|
||||
close(rfd); /* borrowed by open_ro_fd (dup'd internally) */
|
||||
if (!v) return -1; /* kcr3 no longer resolves the kernel => stale/guest-reboot */
|
||||
/* Second, independent signal: the System process must be present AND its cr3 must equal
|
||||
* the persisted kcr3 (the System DTB by definition). Catches the pathology "kcr3 resolves
|
||||
* a DIFFERENT kernel". Cheap — the RO context is already built. Fail-closed on mismatch. */
|
||||
process procs[16];
|
||||
int n = proc_list(v, 0, procs, 16);
|
||||
int system_ok = 0;
|
||||
for (int i = 0; i < n && i < 16; i++)
|
||||
if (!strcmp(procs[i].name, "System")) { system_ok = (procs[i].cr3 == rq->cr3); break; }
|
||||
vmie_win32_close(v); /* validation-only: the read datum needs no held handle */
|
||||
if (!system_ok) return -1;
|
||||
rs->kcr3 = rq->cr3; /* validated: publish the read datum (NOT a->kcr3) */
|
||||
return 0;
|
||||
#else
|
||||
return -1; /* armed without the build flag: resume impossible -> cold bootstrap */
|
||||
#endif
|
||||
}
|
||||
|
||||
/* MC_JOB_BOOTSTRAP */
|
||||
if (a->stub) {
|
||||
/* test-only: fail the first fail_boots attempts to exercise the retry path
|
||||
* deterministically (a->fail_boots is set once in open, read-only here). */
|
||||
if (rq->attempt < a->fail_boots) return -1;
|
||||
rs->kcr3 = 0xC0DE0000ull + (uint64_t)rq->boot_count * 0x1000ull; /* changes per epoch */
|
||||
return 0;
|
||||
}
|
||||
@@ -202,9 +376,49 @@ static void mc_kick_bootstrap(struct vmsig_adapter* a) {
|
||||
mc_req rq;
|
||||
memset(&rq, 0, sizeof rq);
|
||||
rq.op = MC_JOB_BOOTSTRAP; rq.boot_count = a->boot_count;
|
||||
rq.attempt = a->boot_attempts; /* failure index of this kick (loop-thread snapshot) */
|
||||
(void)vmsig_worker_submit(a->worker, &rq, sizeof rq); /* full => drop (rare) */
|
||||
}
|
||||
|
||||
/* Submit the fast-path RESUME (off-loop: open_ro_fd reads image pages, not on the loop thread).
|
||||
* Carries the persisted kcr3 + the cfg low for vmie_win32_open_ro_fd. On miss/validation-fail the
|
||||
* completion handler falls back to a cold bootstrap — the persist never replaces it. */
|
||||
static void mc_kick_resume(struct vmsig_adapter* a, uint64_t kcr3) {
|
||||
mc_req rq;
|
||||
memset(&rq, 0, sizeof rq);
|
||||
rq.op = MC_JOB_RESUME; rq.cr3 = kcr3; rq.low = a->low;
|
||||
(void)vmsig_worker_submit(a->worker, &rq, sizeof rq); /* full => drop (rare) */
|
||||
}
|
||||
|
||||
/* Single publication path for BOTH RESUME and BOOTSTRAP (no two ways to publish a MEMCTX).
|
||||
* Assembles the single-low locator from `kcr3` + a->low, marks have_ctx, and emits the MEMCTX
|
||||
* trigger; the core authoritatively re-describes and stamps the epoch. Loop-thread-only.
|
||||
*
|
||||
* Ownership: this writes kcr3 ONLY into cur_pod.kcr3 (the delivery copy). It does NOT touch
|
||||
* a->kcr3 — that is the gva_write TARGET, owned solely by the bootstrap worker. The difference
|
||||
* between the two callers is only the SOURCE of kcr3 and whether an RW-hold / persist-save
|
||||
* follows; the locator assembly itself is shared here. */
|
||||
static void mc_publish_ctx(struct vmsig_adapter* a, uint64_t kcr3) {
|
||||
memset(&a->cur_pod, 0, sizeof a->cur_pod);
|
||||
a->cur_pod.kcr3 = kcr3;
|
||||
a->cur_pod.low = a->low ? a->low : MC_STUB_SIZE;
|
||||
a->cur_pod.flags = VMSIG_MEMCTX_RDONLY;
|
||||
a->cur_nseg = 1; /* single-low identity (gpa 0 .. low) */
|
||||
a->cur_segs[0].gpa = 0;
|
||||
a->cur_segs[0].len = a->cur_pod.low;
|
||||
a->cur_segs[0].file_off = 0;
|
||||
a->cur_pod.nseg = a->cur_nseg;
|
||||
a->have_ctx = 1;
|
||||
|
||||
/* emit the MEMCTX trigger: the core authoritatively re-describes + stamps the epoch. */
|
||||
vmsig_event up;
|
||||
memset(&up, 0, sizeof up);
|
||||
up.kind = VMSIG_EV_MEMCTX; up.source = VMSIG_SRC_MEMCTX; up.dir = VMSIG_DIR_UP;
|
||||
up.prio = VMSIG_PRIO_NORMAL; up.endpoint = a->endpoint;
|
||||
memcpy(up.inln, &a->cur_pod, sizeof a->cur_pod);
|
||||
a->emit.emit(a->emit.token, &up);
|
||||
}
|
||||
|
||||
/* ---- reg hooks (vmsig_memctx_reg.ctx = a; called by the core on the loop thread) ---- */
|
||||
static void mc_reg_describe(void* ctx, vmsig_memctx* out_pod,
|
||||
const vmsig_memseg** out_segs, uint32_t* out_nseg) {
|
||||
@@ -232,6 +446,14 @@ static void mc_reg_invalidate(void* ctx, uint32_t epoch) {
|
||||
struct vmsig_adapter* a = ctx;
|
||||
(void)epoch; /* the core owns the epoch; the adapter must re-bootstrap */
|
||||
a->have_ctx = 0; /* the previous context is invalid */
|
||||
/* destructive VM-lifecycle => the RAM may have changed => drop the kcr3 cache so the next
|
||||
* restart cannot fast-path off a now-dead kcr3 (the self-validation would reject it anyway,
|
||||
* but we do not leave a known-stale file). Best-effort, loop-thread-only. */
|
||||
mc_persist_drop(a->persist_path);
|
||||
/* new cycle: drop a stale arm from the previous cycle and restart the failure counter at
|
||||
* zero so this bootstrap's backoff starts fresh (and the first-failure diagnostic re-arms). */
|
||||
a->boot_attempts = 0;
|
||||
mc_disarm_retry(a);
|
||||
mc_kick_bootstrap(a); /* off-loop; on_ready re-emits MEMCTX (new epoch) */
|
||||
}
|
||||
|
||||
@@ -247,11 +469,14 @@ static vmsig_adapter* mc_open(const void* cfg, uint32_t endpoint) {
|
||||
a->cfg_ro_fd = (c && c->ro_fd >= 0) ? c->ro_fd : -1;
|
||||
if (!a->ram_path && a->cfg_ro_fd < 0) a->stub = 1; /* no path/fd => stub */
|
||||
a->stub_fd = -1;
|
||||
a->retry_fd = -1;
|
||||
a->fail_boots = c ? c->fail_boots : 0; /* set once; read-only afterwards (worker reads) */
|
||||
a->persist_path = c ? c->persist_path : NULL; /* NULL => persist disabled (cold bootstrap only) */
|
||||
return a;
|
||||
}
|
||||
|
||||
static int mc_attach(vmsig_adapter* a, const vmsig_emit* emit, vmsig_fd_reg* reg, int cap) {
|
||||
if (cap < 1) return -1;
|
||||
if (cap < 2) return -1; /* worker eventfd + one-shot backoff timerfd */
|
||||
a->emit = *emit;
|
||||
|
||||
a->worker = vmsig_worker_new(mc_job, a, 1, MC_WORKER_DEPTH);
|
||||
@@ -262,11 +487,27 @@ static int mc_attach(vmsig_adapter* a, const vmsig_emit* emit, vmsig_fd_reg* reg
|
||||
if (a->stub_fd < 0) { vmsig_worker_free(a->worker); a->worker = NULL; return -1; }
|
||||
}
|
||||
|
||||
/* worker completion-eventfd as the readiness source (cookie=0). */
|
||||
/* one-shot backoff timerfd: re-kicks the cold bootstrap when the guest is still booting.
|
||||
* Created here (loop-thread-only fd); armed on failure, disarmed on success. Rollback the
|
||||
* worker + stub_fd on failure, symmetric to mc_make_stub_fd above. */
|
||||
a->retry_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
|
||||
if (a->retry_fd < 0) {
|
||||
if (a->stub_fd >= 0) { close(a->stub_fd); a->stub_fd = -1; }
|
||||
vmsig_worker_free(a->worker); a->worker = NULL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* worker completion-eventfd as the readiness source (cookie=worker). */
|
||||
reg[0].fd = vmsig_worker_evfd(a->worker);
|
||||
reg[0].epoll_events = EPOLLIN;
|
||||
reg[0].shape = VMSIG_RDY_EVENTFD;
|
||||
reg[0].cookie = 0;
|
||||
reg[0].cookie = MC_COOKIE_WORKER;
|
||||
|
||||
/* backoff timerfd as the second readiness source (cookie=retry). */
|
||||
reg[1].fd = a->retry_fd;
|
||||
reg[1].epoll_events = EPOLLIN;
|
||||
reg[1].shape = VMSIG_RDY_TIMERFD;
|
||||
reg[1].cookie = MC_COOKIE_RETRY;
|
||||
|
||||
/* register the reg BEFORE the first bootstrap: the core slot gets the hooks. describe
|
||||
* is not called until the slot is valid (which only happens after the first MEMCTX). */
|
||||
@@ -288,12 +529,33 @@ static int mc_attach(vmsig_adapter* a, const vmsig_emit* emit, vmsig_fd_reg* reg
|
||||
up.prio = VMSIG_PRIO_NORMAL; up.endpoint = a->endpoint;
|
||||
a->emit.emit(a->emit.token, &up);
|
||||
|
||||
mc_kick_bootstrap(a); /* first bootstrap off-loop; assemble the locator on completion */
|
||||
return 1;
|
||||
/* Fast-path: if a kcr3 cache exists, try a RESUME (re-validate it against live RAM) BEFORE
|
||||
* the cold scan. On a daemon restart over an unchanged guest this publishes the read datum
|
||||
* in milliseconds instead of minutes of beacon-scan retry. On any miss (persist off / stub /
|
||||
* no file / corrupt) we fall straight into the existing cold bootstrap. The RW-hold for
|
||||
* MEMWRITE is still acquired by a cold bootstrap (kicked in parallel after a RESUME hit). */
|
||||
mc_persist_blob b;
|
||||
if (a->persist_path && *a->persist_path && mc_persist_load(a->persist_path, &b))
|
||||
mc_kick_resume(a, b.kcr3); /* validate the cached kcr3 off-loop; cold fallback on miss */
|
||||
else
|
||||
mc_kick_bootstrap(a); /* first cold bootstrap off-loop; assemble locator on completion */
|
||||
return 2; /* worker eventfd + backoff timerfd */
|
||||
}
|
||||
|
||||
static int mc_on_ready(vmsig_adapter* a, uint32_t cookie, uint32_t events) {
|
||||
(void)cookie; (void)events;
|
||||
(void)events; /* epoll flags carry nothing we need; the cookie selects the source */
|
||||
|
||||
/* retry timerfd fired: the guest was still booting; drain and re-kick the bootstrap.
|
||||
* Re-kick is a fresh MC_JOB_BOOTSTRAP into the SAME FIFO worker queue, so it serializes
|
||||
* behind any in-flight write — nothing extra to synchronize. */
|
||||
if (cookie == MC_COOKIE_RETRY) {
|
||||
uint64_t v;
|
||||
while (read(a->retry_fd, &v, sizeof v) == (ssize_t)sizeof v) { /* drain to EAGAIN */ }
|
||||
mc_kick_bootstrap(a);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* cookie == MC_COOKIE_WORKER: worker completion. */
|
||||
vmsig_worker_ack(a->worker);
|
||||
mc_res rs;
|
||||
int rc;
|
||||
@@ -303,37 +565,64 @@ static int mc_on_ready(vmsig_adapter* a, uint32_t cookie, uint32_t events) {
|
||||
mc_memwrite_ack(a, rs.ok && rc == 0, rs.corr, rs.origin);
|
||||
continue;
|
||||
}
|
||||
if (rc != 0) {
|
||||
/* bootstrap failed: ERROR (source MEMCTX); do NOT publish an invalid kcr3. */
|
||||
vmsig_event er;
|
||||
memset(&er, 0, sizeof er);
|
||||
er.kind = VMSIG_EV_ERROR; er.source = VMSIG_SRC_MEMCTX; er.dir = VMSIG_DIR_UP;
|
||||
er.prio = VMSIG_PRIO_URGENT; er.endpoint = a->endpoint;
|
||||
a->emit.emit(a->emit.token, &er);
|
||||
|
||||
if (rs.op == MC_JOB_RESUME) {
|
||||
/* Fast-path completion. The persisted kcr3 was validated against the LIVE RAM on the
|
||||
* worker (open_ro_fd != NULL [+ System-cr3 match]) — the read datum is safe to publish.
|
||||
* Note: the worker did NOT set a->kcr3/a->win/a->mem (the RW write-hold), so MEMWRITE
|
||||
* stays ok=0 until a cold bootstrap acquires it. */
|
||||
if (rc == 0) {
|
||||
mc_publish_ctx(a, rs.kcr3); /* video lives instantly (read datum), epoch by core */
|
||||
mc_kick_bootstrap(a); /* in parallel: acquire the RW-hold (a->kcr3) for MEMWRITE */
|
||||
/* Do NOT save the persist (the kcr3 came FROM the file) and do NOT arm a retry
|
||||
* (the read datum is up; the parallel bootstrap arms its own retry on failure). */
|
||||
} else {
|
||||
/* validation miss: the persisted kcr3 no longer resolves the kernel (guest rebooted
|
||||
* or corrupt). Fall back to an honest cold scan; on success it rewrites the persist
|
||||
* with a fresh kcr3. Do NOT retry the RESUME — the cache is under suspicion. */
|
||||
mc_kick_bootstrap(a);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
/* assemble the locator on the loop thread from rs.kcr3. a->kcr3 is the gva_write
|
||||
* TARGET and is owned SOLELY by the worker thread (set in mc_bootstrap_armed, read by
|
||||
* MC_JOB_WRITE — same thread, FIFO happens-before); the loop must NOT also write it, or
|
||||
* an in-flight write at line ~170 would race it. cur_pod.kcr3 is loop-only (delivery). */
|
||||
memset(&a->cur_pod, 0, sizeof a->cur_pod);
|
||||
a->cur_pod.kcr3 = rs.kcr3;
|
||||
a->cur_pod.low = a->low ? a->low : MC_STUB_SIZE;
|
||||
a->cur_pod.flags = VMSIG_MEMCTX_RDONLY;
|
||||
a->cur_nseg = 1; /* single-low identity (gpa 0 .. low) */
|
||||
a->cur_segs[0].gpa = 0;
|
||||
a->cur_segs[0].len = a->cur_pod.low;
|
||||
a->cur_segs[0].file_off = 0;
|
||||
a->cur_pod.nseg = a->cur_nseg;
|
||||
a->have_ctx = 1;
|
||||
|
||||
/* emit the MEMCTX trigger: the core authoritatively re-describes + stamps the epoch. */
|
||||
vmsig_event up;
|
||||
memset(&up, 0, sizeof up);
|
||||
up.kind = VMSIG_EV_MEMCTX; up.source = VMSIG_SRC_MEMCTX; up.dir = VMSIG_DIR_UP;
|
||||
up.prio = VMSIG_PRIO_NORMAL; up.endpoint = a->endpoint;
|
||||
memcpy(up.inln, &a->cur_pod, sizeof a->cur_pod);
|
||||
a->emit.emit(a->emit.token, &up);
|
||||
/* MC_JOB_BOOTSTRAP */
|
||||
if (rc != 0) {
|
||||
/* bootstrap failed: the guest is likely still booting (host_bootstrap found no
|
||||
* System process). This is NOT a control-level error — do NOT emit VMSIG_EV_ERROR
|
||||
* (it would spam URGENT during a normal multi-second guest boot). Instead schedule a
|
||||
* backoff retry; the context simply stays unpublished until a kick succeeds. One
|
||||
* diagnostic line on the FIRST failure of the cycle (symmetric to the discovery
|
||||
* "never came up" note), not on every attempt. */
|
||||
if (a->boot_attempts == 0)
|
||||
fprintf(stderr, "vmsig memctx: endpoint %u bootstrap not ready yet, retrying\n",
|
||||
a->endpoint);
|
||||
a->boot_attempts++;
|
||||
mc_arm_retry(a); /* one-shot timer at mc_boot_backoff(boot_attempts) */
|
||||
continue;
|
||||
}
|
||||
/* bootstrap succeeded: a->kcr3/a->mem (the gva_write TARGET / RW-hold) were set on the
|
||||
* worker (mc_bootstrap_armed); the loop must NOT also write a->kcr3 (it would race an
|
||||
* in-flight write — same FIFO thread owns it). MEMWRITE is now possible. cur_pod.kcr3 is
|
||||
* loop-only (delivery) and is set inside mc_publish_ctx.
|
||||
*
|
||||
* Cancel any pending retry and reset the failure counter BEFORE publishing, so a stale
|
||||
* timer armed by a prior failure cannot fire over a live context. */
|
||||
a->boot_attempts = 0;
|
||||
mc_disarm_retry(a);
|
||||
|
||||
/* Publish only if a RESUME has not already published this same context (same kcr3): a
|
||||
* parallel cold bootstrap after a RESUME hit must acquire the RW-hold WITHOUT emitting a
|
||||
* redundant MEMCTX. First-time publication otherwise. */
|
||||
if (!a->have_ctx)
|
||||
mc_publish_ctx(a, rs.kcr3);
|
||||
|
||||
/* Cache the freshly-scanned kcr3 for the next daemon restart (best-effort; the datum is
|
||||
* already published). Only the cold scan writes the persist — never the RESUME path (its
|
||||
* kcr3 came from the file). Gated on persist_path presence: production stub paths get a
|
||||
* NULL persist_path from discovery, so they never write; a test may supply one to exercise
|
||||
* the persist mechanics (the stub bootstrap yields a synthetic-but-stable kcr3). */
|
||||
if (a->persist_path && *a->persist_path)
|
||||
(void)mc_persist_save(a->persist_path, rs.kcr3);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -398,6 +687,9 @@ static void mc_close(vmsig_adapter* a) {
|
||||
if (a->win) vmie_win32_close(a->win); /* AFTER worker join: no in-flight gva_write */
|
||||
#endif
|
||||
if (a->stub_fd >= 0) close(a->stub_fd);
|
||||
/* one-shot backoff timerfd: never spawns a worker job, so its close is independent of the
|
||||
* worker join — same contract as stub_fd. The core already epoll_ctl(DEL)'d the slot. */
|
||||
if (a->retry_fd >= 0) close(a->retry_fd);
|
||||
/* ro_fd ownership transferred to the adapter at open(): close it here so a re-grant
|
||||
* (detach + re-attach with a fresh infra ro_fd) does not leak the prior one. Infra
|
||||
* that must keep its own copy dups before handing it in — symmetric to the holder
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
typedef struct {
|
||||
int stub;
|
||||
const char* qmp_path;
|
||||
/* Host->guest input bridge: evdev node paths of the uinput devices (published by the input
|
||||
* seam). When non-NULL/non-empty, on reaching READY the seam adds an input-linux QMP object
|
||||
* forwarding them into the guest (A=keyboard with grab_all, B=relative mouse). NULL/"" => no bridge
|
||||
* (stub/tests are fail-closed). Pointers are borrowed from the stable per-endpoint home and
|
||||
* outlive the adapter. */
|
||||
const char* bridge_evdev_a;
|
||||
const char* bridge_evdev_b;
|
||||
} vmsig_vmhost_cfg;
|
||||
|
||||
#endif /* VMSIG_VMHOST_H */
|
||||
|
||||
@@ -27,6 +27,12 @@
|
||||
|
||||
enum { ST_STUB = 0, ST_CONNECTING, ST_NEGOTIATING, ST_READY, ST_DEAD };
|
||||
|
||||
/* Internal pend op tags for the host->guest input bridge (object_add/object_del). These are
|
||||
* NOT VMSIG_VMOP_* (which occupy 0..5) and never reach control: bridge setup is the seam's own
|
||||
* VM-substrate infrastructure, so its replies are handled silently (no ACK, no VM_LIFECYCLE). */
|
||||
#define VH_OP_BRIDGE_ADD 0x80
|
||||
#define VH_OP_BRIDGE_DEL 0x81
|
||||
|
||||
typedef struct { uint32_t id, origin, corr; uint8_t op; int used; } pend_ent;
|
||||
|
||||
struct vmsig_adapter {
|
||||
@@ -41,6 +47,13 @@ struct vmsig_adapter {
|
||||
size_t buflen;
|
||||
uint32_t next_id;
|
||||
pend_ent pend[VMHOST_MAX_PENDING];
|
||||
/* Host->guest input bridge: evdev paths borrowed from the stable per-endpoint home;
|
||||
* NULL/"" => no bridge. The *_up flags track which input-linux objects were added so
|
||||
* teardown can object_del exactly those. */
|
||||
const char* bridge_evdev_a;
|
||||
const char* bridge_evdev_b;
|
||||
int bridge_a_up;
|
||||
int bridge_b_up;
|
||||
};
|
||||
|
||||
/* ---- minimal QMP line parse (top-level keys only; full JSON — deferred) ---- */
|
||||
@@ -101,6 +114,71 @@ static pend_ent* pend_find(struct vmsig_adapter* a, uint32_t id) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---- host->guest input bridge (input-linux object_add/object_del) ---- */
|
||||
|
||||
/* Neutral, per-endpoint object ids — no private paths or names; evdev comes from cfg. */
|
||||
static void bridge_id(char* out, size_t cap, uint32_t ep, char ab) {
|
||||
snprintf(out, cap, "vmsig-in-%c-%u", ab, ep);
|
||||
}
|
||||
|
||||
/* Best-effort object-del of a possibly-stale object id (defined below; fwd for bridge_add). */
|
||||
static void bridge_del_fire(struct vmsig_adapter* a, char ab);
|
||||
|
||||
/* Add one input-linux object forwarding an evdev node into the guest. grab_all toggles the
|
||||
* device-grab for every input-linux on this endpoint (set on A only — one is enough). The
|
||||
* reply is correlated through the existing pend[] table under VH_OP_BRIDGE_ADD and consumed
|
||||
* silently. Returns 0 on a queued write, -1 on backpressure / write failure. */
|
||||
static int bridge_add(struct vmsig_adapter* a, char ab, const char* evdev, int grab_all) {
|
||||
pend_ent* p = pend_alloc(a);
|
||||
if (!p) return -1;
|
||||
char id[32];
|
||||
bridge_id(id, sizeof id, a->endpoint, ab);
|
||||
/* Idempotent re-attach: fire object-del for this id FIRST. A prior daemon instance tears the
|
||||
* bridge down best-effort WITHOUT a round-trip (bridge_del_fire in vh_close), and a fast
|
||||
* restart/redeploy can reach here before QEMU processed that del — leaving the object live,
|
||||
* so a bare object-add fails with "duplicate property '<id>'" (observed for device B). QMP is
|
||||
* sequential per connection, so this del is applied before the add below; on a clean first
|
||||
* attach it just no-ops (DeviceNotFound, silently dropped — that frame carries no QMP id). */
|
||||
bridge_del_fire(a, ab);
|
||||
uint32_t qid = ++a->next_id;
|
||||
char line[320];
|
||||
int len = snprintf(line, sizeof line,
|
||||
/* QMP object commands use HYPHENS (object-add/object-del); only the legacy
|
||||
* device_add/device_del keep underscores. Underscore here => CommandNotFound. */
|
||||
"{\"execute\":\"object-add\",\"arguments\":{\"qom-type\":\"input-linux\","
|
||||
"\"id\":\"%s\",\"evdev\":\"%s\"%s},\"id\":%u}\r\n",
|
||||
id, evdev, grab_all ? ",\"grab_all\":true" : "", qid);
|
||||
p->used = 1; p->id = qid; p->origin = 0; p->corr = 0; p->op = VH_OP_BRIDGE_ADD;
|
||||
ssize_t r = write(a->fd, line, (size_t)len);
|
||||
if (r != (ssize_t)len) { p->used = 0; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Best-effort object-del, no reply awaited. Fired in TWO places: on teardown before the fd
|
||||
* closes (vh_close) AND before every object-add (bridge_add, idempotent re-attach). QEMU drops
|
||||
* these objects when the VM powers off, so del matters on a detach/re-attach without power-off
|
||||
* (daemon restart / endpoint move). A del of an absent id no-ops (DeviceNotFound, silently
|
||||
* dropped — this frame carries no QMP id, so handle_line finds no pend). */
|
||||
static void bridge_del_fire(struct vmsig_adapter* a, char ab) {
|
||||
char id[32];
|
||||
bridge_id(id, sizeof id, a->endpoint, ab);
|
||||
char line[160];
|
||||
int len = snprintf(line, sizeof line,
|
||||
"{\"execute\":\"object-del\",\"arguments\":{\"id\":\"%s\"}}\r\n", id);
|
||||
ssize_t r = write(a->fd, line, (size_t)len);
|
||||
(void)r;
|
||||
}
|
||||
|
||||
/* Add the bridge objects upon reaching READY. A is added with grab_all; B (mouse) without. */
|
||||
static void bridge_on_ready(struct vmsig_adapter* a) {
|
||||
if (a->bridge_evdev_a && a->bridge_evdev_a[0]) {
|
||||
if (bridge_add(a, 'a', a->bridge_evdev_a, 1) == 0) a->bridge_a_up = 1;
|
||||
}
|
||||
if (a->bridge_evdev_b && a->bridge_evdev_b[0]) {
|
||||
if (bridge_add(a, 'b', a->bridge_evdev_b, 0) == 0) a->bridge_b_up = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- emission of neutral UP events ---- */
|
||||
static void emit_vm(struct vmsig_adapter* a, uint32_t state, uint32_t origin, uint32_t corr) {
|
||||
vmsig_vm_state vs = { state, 0 };
|
||||
@@ -142,7 +220,11 @@ static void handle_line(struct vmsig_adapter* a, const char* line) {
|
||||
}
|
||||
break;
|
||||
case ST_NEGOTIATING:
|
||||
if (strstr(line, "\"return\"")) { a->st = ST_READY; emit_seam(a, VMSIG_EV_SEAM_UP); }
|
||||
if (strstr(line, "\"return\"")) {
|
||||
a->st = ST_READY;
|
||||
emit_seam(a, VMSIG_EV_SEAM_UP);
|
||||
bridge_on_ready(a); /* forward the host uinput evdev nodes into the guest */
|
||||
}
|
||||
break;
|
||||
case ST_READY:
|
||||
if (strstr(line, "\"event\"")) {
|
||||
@@ -155,7 +237,12 @@ static void handle_line(struct vmsig_adapter* a, const char* line) {
|
||||
long id = jnum(line, "\"id\"");
|
||||
pend_ent* p = id >= 0 ? pend_find(a, (uint32_t)id) : NULL;
|
||||
if (p) {
|
||||
if (p->op == VMSIG_VMOP_QUERY && strstr(line, "\"return\"")) {
|
||||
if (p->op == VH_OP_BRIDGE_ADD || p->op == VH_OP_BRIDGE_DEL) {
|
||||
/* Bridge infrastructure: never surfaces to control. Log a failed add so the
|
||||
* stand can see it; otherwise silent. */
|
||||
if (p->op == VH_OP_BRIDGE_ADD && strstr(line, "\"error\""))
|
||||
fprintf(stderr, "vmsig vmhost: input-linux object-add failed: %s\n", line);
|
||||
} else if (p->op == VMSIG_VMOP_QUERY && strstr(line, "\"return\"")) {
|
||||
char stbuf[32]; uint32_t s = VMSIG_VM_UNKNOWN;
|
||||
if (jstr(line, "\"status\"", stbuf, sizeof stbuf)) s = status_state(stbuf);
|
||||
emit_vm(a, s, p->origin, p->corr); /* addressed reply */
|
||||
@@ -184,6 +271,7 @@ static vmsig_adapter* vh_open(const void* cfg, uint32_t endpoint) {
|
||||
a->endpoint = endpoint;
|
||||
a->qmp_path = (c && c->qmp_path && c->qmp_path[0]) ? c->qmp_path : NULL;
|
||||
a->stub = (a->qmp_path == NULL); /* path given => armed, otherwise stub */
|
||||
if (c) { a->bridge_evdev_a = c->bridge_evdev_a; a->bridge_evdev_b = c->bridge_evdev_b; }
|
||||
a->fd = -1;
|
||||
a->cur = VMSIG_VM_RUNNING;
|
||||
return a;
|
||||
@@ -300,6 +388,12 @@ static int vh_submit(vmsig_adapter* a, const vmsig_event* ev) {
|
||||
|
||||
static void vh_close(vmsig_adapter* a) {
|
||||
if (!a) return;
|
||||
/* Best-effort teardown of the input-linux bridge while the connection is still live: fire
|
||||
* object_del for the objects we added, with no round-trip (the fd closes right after). */
|
||||
if (a->st == ST_READY && a->fd >= 0) {
|
||||
if (a->bridge_a_up) bridge_del_fire(a, 'a');
|
||||
if (a->bridge_b_up) bridge_del_fire(a, 'b');
|
||||
}
|
||||
if (a->fd >= 0) close(a->fd);
|
||||
free(a);
|
||||
}
|
||||
|
||||
@@ -106,9 +106,9 @@ static int on_event(void* user, const vmsig_event* ev) {
|
||||
in.prio = VMSIG_PRIO_HIGH; in.endpoint = 0; in.corr = 0xC0FFEEu;
|
||||
in.payload.flags = VMSIG_PL_INLINE;
|
||||
vmsig_input act; memset(&act, 0, sizeof act); /* neutral public input contract */
|
||||
act.kind = VMSIG_INPUT_MOVE_ABS; act.x = 100; act.y = 100; /* demo: abs pointer (100,100) */
|
||||
act.kind = VMSIG_INPUT_MOVE_REL; act.x = 5; act.y = 5; /* demo: relative move (dx=5,dy=5) */
|
||||
memcpy(in.inln, &act, sizeof act);
|
||||
printf(" DOWN CMD_INPUT MOVE_ABS x=100 y=100 corr=0x%X\n", (unsigned)in.corr);
|
||||
printf(" DOWN CMD_INPUT MOVE_REL dx=5 dy=5 corr=0x%X\n", (unsigned)in.corr);
|
||||
vmsig_inproc_send(d->ctl, &in);
|
||||
|
||||
vmsig_event vm;
|
||||
|
||||
@@ -56,6 +56,14 @@ struct vmsig_discovery {
|
||||
* keep pointers, and detach is deferred, so this must outlive the candidate. Overwritten
|
||||
* only on the NEXT attach to the endpoint, which never races a still-open prior adapter. */
|
||||
vmsig_host_facts ep_facts[VMSIG_SLOT_COUNT];
|
||||
/* Stable per-endpoint home for the uinput evdev paths of the input bridge. The input seam
|
||||
* writes these at attach; the vmhost seam borrows them to add input-linux objects. Same
|
||||
* lifetime discipline as ep_facts (outlives the deferred adapter reap). */
|
||||
struct { char evdev_a[64]; char evdev_b[64]; } ep_bridge[VMSIG_SLOT_COUNT];
|
||||
/* Stable per-endpoint home for the memctx kcr3-cache path (sibling of .slots in the watch
|
||||
* dir). The memctx adapter keeps the pointer across its lifetime; same lifetime discipline
|
||||
* as ep_facts/ep_bridge (outlives the deferred adapter reap, overwritten on next attach). */
|
||||
char ep_persist[VMSIG_SLOT_COUNT][DISC_PATH_MAX + 32];
|
||||
};
|
||||
|
||||
static uint64_t now_ns(void) {
|
||||
@@ -265,17 +273,38 @@ static void bootstrap_scan(vmsig_discovery* d) {
|
||||
|
||||
static int default_attach(void* ud, vmsig_core* core, uint32_t vmid, uint32_t endpoint,
|
||||
const vmsig_host_facts* f) {
|
||||
(void)ud; (void)vmid;
|
||||
vmsig_discovery* d = ud; /* default sink carries the discovery handle (ep_bridge home) */
|
||||
char* ev_a = d ? d->ep_bridge[endpoint].evdev_a : NULL;
|
||||
char* ev_b = d ? d->ep_bridge[endpoint].evdev_b : NULL;
|
||||
if (d) { ev_a[0] = '\0'; ev_b[0] = '\0'; } /* clear stale paths from a prior attach */
|
||||
|
||||
/* Form the kcr3-cache path (per-vmid, sibling of .slots/the RAM file in the watch dir).
|
||||
* Gated on d->persist — one policy for all ephemeral watch-dir state. NULL => persist off. */
|
||||
const char* persist_path = NULL;
|
||||
if (d && d->persist) {
|
||||
int pn = snprintf(d->ep_persist[endpoint], sizeof d->ep_persist[endpoint],
|
||||
"%s/.kcr3-vm-%u", d->watch_dir, vmid);
|
||||
/* only enable the cache if the path fit (a truncated path would point elsewhere). */
|
||||
if (pn > 0 && (size_t)pn < sizeof d->ep_persist[endpoint])
|
||||
persist_path = d->ep_persist[endpoint];
|
||||
}
|
||||
|
||||
vmsig_memctx_cfg mc; memset(&mc, 0, sizeof mc);
|
||||
mc.stub = 0; mc.ram_path = f->ram_path; mc.low = f->low; mc.ro_fd = -1;
|
||||
vmsig_vmhost_cfg vh; memset(&vh, 0, sizeof vh);
|
||||
vh.stub = 0; vh.qmp_path = f->qmp_path;
|
||||
mc.persist_path = persist_path;
|
||||
vmsig_input_cfg in; memset(&in, 0, sizeof in);
|
||||
in.stub = 0; in.qmp_path = NULL; /* input is uinput; power/lifecycle via the vmhost seam */
|
||||
/* input is uinput; power/lifecycle via the vmhost seam. The adapter publishes its uinput
|
||||
* evdev paths into ep_bridge so the vmhost seam can forward them via input-linux. */
|
||||
in.stub = 0; in.qmp_path = NULL; in.out_evdev_a = ev_a; in.out_evdev_b = ev_b;
|
||||
vmsig_vmhost_cfg vh; memset(&vh, 0, sizeof vh);
|
||||
/* vmhost borrows the (now-populated) evdev paths to add the input-linux bridge at READY. */
|
||||
vh.stub = 0; vh.qmp_path = f->qmp_path; vh.bridge_evdev_a = ev_a; vh.bridge_evdev_b = ev_b;
|
||||
|
||||
/* Order matters: input attaches BEFORE vmhost so the evdev paths are written into ep_bridge
|
||||
* before the vmhost seam reads them (READY is async and always later than both attaches). */
|
||||
if (vmsig_core_add_adapter(core, vmsig_memctx_ops(), &mc, endpoint) < 0) goto fail;
|
||||
if (vmsig_core_add_adapter(core, vmsig_vmhost_ops(), &vh, endpoint) < 0) goto fail;
|
||||
if (vmsig_core_add_adapter(core, vmsig_input_ops(), &in, endpoint) < 0) goto fail;
|
||||
if (vmsig_core_add_adapter(core, vmsig_vmhost_ops(), &vh, endpoint) < 0) goto fail;
|
||||
return 0;
|
||||
fail:
|
||||
vmsig_core_detach_endpoint(core, endpoint); /* roll back any partial trio (deferred) */
|
||||
@@ -319,7 +348,7 @@ vmsig_discovery* vmsig_discovery_new(vmsig_core* core,
|
||||
pve_conf ? pve_conf : "/etc/pve/qemu-server",
|
||||
qmp_dir ? qmp_dir : "/var/run/qemu-server");
|
||||
if (sink) d->sink = *sink;
|
||||
else { d->sink.attach = default_attach; d->sink.detach = default_detach; d->sink.ud = NULL; }
|
||||
else { d->sink.attach = default_attach; d->sink.detach = default_detach; d->sink.ud = d; }
|
||||
|
||||
slot_load(&d->slots, d->persist ? d->slots_path : NULL);
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef VMSIG_MTREE_H
|
||||
#define VMSIG_MTREE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Derive the below-4G split (== vmie `low`: low-RAM GPA bound AND high-RAM file offset)
|
||||
* from `info mtree -f` text. Operates on the system flatview only. FAIL-CLOSED: 0 if the
|
||||
* split cannot be derived with confidence. `text` is plain UTF-8 with real '\n'
|
||||
* (the caller un-escapes the QMP JSON string first). */
|
||||
uint64_t mtree_low_split(const char* text);
|
||||
|
||||
#endif /* VMSIG_MTREE_H */
|
||||
@@ -5,6 +5,7 @@
|
||||
* leaves ok=0 (the VM is not brought up rather than guessed). */
|
||||
#define _GNU_SOURCE
|
||||
#include "host_probe.h"
|
||||
#include "mtree.h" /* mtree_low_split */
|
||||
#include "vmsig_event.h" /* VMSIG_VM_* */
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -174,29 +175,39 @@ static int qmp_status_word(const char* buf) {
|
||||
return VMSIG_VM_UNKNOWN;
|
||||
}
|
||||
|
||||
/* Derive the below-4G split from `info mtree` text: the size of the RAM region whose guest
|
||||
* physical range starts at address 0. Standard QEMU split-RAM layout puts low RAM at
|
||||
* [0, low) and high RAM above 4G at file offset @low. FAIL-CLOSED: 0 if not found.
|
||||
* NOTE: parses HMP text (not a stable QMP schema) — verify against real `info mtree` output. */
|
||||
static uint64_t mtree_low(const char* ret) {
|
||||
/* The return is a JSON string; lines inside are escaped "\n". Scan for the GPA-0 ram run:
|
||||
* " 0000000000000000-<end16> (prio N, ram): ..." */
|
||||
const char* p = ret;
|
||||
while ((p = strstr(p, "0000000000000000-")) != NULL) {
|
||||
const char* end_hex = p + 17; /* 16 zeros + '-' */
|
||||
char* stop = NULL;
|
||||
unsigned long long end = strtoull(end_hex, &stop, 16);
|
||||
/* the descriptor after the range must mark it RAM (not the i/o "system" root) */
|
||||
const char* tail = stop ? stop : end_hex;
|
||||
const char* nl = strstr(tail, "\\n");
|
||||
const char* lim = nl ? nl : (tail + 64);
|
||||
int is_ram = 0;
|
||||
for (const char* q = tail; q < lim && *q; q++)
|
||||
if (!strncmp(q, "ram)", 4)) { is_ram = 1; break; }
|
||||
if (is_ram && end > 0 && end != ~0ull) return end + 1ull; /* [0, end] => low=end+1 */
|
||||
p = end_hex;
|
||||
/* Extract the JSON string value of "return" from an HMP-over-QMP reply and decode its
|
||||
* transport escapes (\n \t \" \\) in place into a NUL-terminated plain-text buffer. The
|
||||
* `info mtree -f` output is one JSON string with embedded escaped newlines; un-escaping is
|
||||
* a transport detail of HMP-over-QMP and belongs here (next to the QMP code), so the split
|
||||
* parser (mtree_low_split) can work on human-readable text with real '\n'. The decode never
|
||||
* grows the buffer (every escape shortens it), so it writes into `out` (>= strlen(buf)+1).
|
||||
* Returns 1 on success, 0 if no "return" string is present. */
|
||||
static int qmp_return_plain(const char* buf, char* out, size_t cap) {
|
||||
const char* r = strstr(buf, "\"return\"");
|
||||
if (!r) return 0;
|
||||
r = strchr(r, ':'); if (!r) return 0;
|
||||
r = strchr(r, '"'); if (!r) return 0; /* opening quote of the string value */
|
||||
r++;
|
||||
size_t o = 0;
|
||||
for (; *r && o + 1 < cap; r++) {
|
||||
char c = *r;
|
||||
if (c == '"') break; /* closing quote */
|
||||
if (c == '\\' && r[1]) {
|
||||
r++;
|
||||
switch (*r) {
|
||||
case 'n': c = '\n'; break;
|
||||
case 't': c = '\t'; break;
|
||||
case 'r': c = '\r'; break;
|
||||
case '"': c = '"'; break;
|
||||
case '\\': c = '\\'; break;
|
||||
case '/': c = '/'; break;
|
||||
default: c = *r; break; /* unknown escape: take it literally */
|
||||
}
|
||||
}
|
||||
out[o++] = c;
|
||||
}
|
||||
return 0;
|
||||
out[o] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int hp_live(const struct vmsig_host_probe* p, vmsig_host_facts* io) {
|
||||
@@ -221,7 +232,9 @@ static int hp_live(const struct vmsig_host_probe* p, vmsig_host_facts* io) {
|
||||
if (qmp_cmd(fd,
|
||||
"{\"execute\":\"human-monitor-command\","
|
||||
"\"arguments\":{\"command-line\":\"info mtree -f\"}}\n", buf, 256 * 1024) == 1) {
|
||||
io->low = mtree_low(buf);
|
||||
/* un-escape the HMP string in place (it only shrinks), then parse the split */
|
||||
if (qmp_return_plain(buf, buf, 256 * 1024))
|
||||
io->low = mtree_low_split(buf);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/* mtree.c — derive the below-4G split (vmie `low`) from `info mtree -f` text.
|
||||
*
|
||||
* `low` is one number with two meanings (see vmie low_segs): the GPA bound of low-RAM
|
||||
* ([0,low) maps 1:1 to file[0,low)) AND the file offset at which RAM resumes above 4 GiB
|
||||
* (GPA 4GiB -> file[low]). The robust signal for it is therefore the `@<file_off>` suffix
|
||||
* of the high-RAM ram region (GPA >= 4 GiB): that offset IS `low` by construction.
|
||||
*
|
||||
* Low-RAM below 4 GiB is fragmented (Hyper-V synic overlays, smbase/tseg blackhole i/o
|
||||
* holes, rom holes), so "end of the first contiguous ram run" is NOT a reliable split.
|
||||
* We never trust it. Primary signal: high-RAM `@offset`. Cross-validator / fallback:
|
||||
* the start GPA of the first non-ram region at or above the standard PCI-hole base
|
||||
* (0x80000000) — the bottom of the 4 GiB PCI hole, which equals `low` for the classic
|
||||
* single-`low` layout. The two must agree when both are present; otherwise fail-closed.
|
||||
*
|
||||
* Pure text, line by line, no allocation beyond the input, no I/O. FAIL-CLOSED: any
|
||||
* unexpected/incomplete input yields 0 ("not found"); 0 is reserved for that. */
|
||||
#include "mtree.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* Standard QEMU/i440fx/q35 PCI-hole base (bottom of the 4 GiB hole). Used ONLY as the
|
||||
* lower cutoff for the cross-validator/fallback, never hardcoded as the answer. */
|
||||
#define PCI_HOLE_BASE 0x80000000ull
|
||||
/* 4 GiB: high-RAM (the ram region carrying `@low`) starts at or above this GPA. */
|
||||
#define RAM_HIGH_BASE 0x100000000ull
|
||||
|
||||
/* Parse exactly `n` hex digits at p into *out. Returns the char past the last digit, or
|
||||
* NULL if there are not n hex digits (no partial consume). */
|
||||
static const char* parse_hexn(const char* p, int n, uint64_t* out) {
|
||||
uint64_t v = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
char c = p[i];
|
||||
unsigned d;
|
||||
if (c >= '0' && c <= '9') d = (unsigned)(c - '0');
|
||||
else if (c >= 'a' && c <= 'f') d = (unsigned)(c - 'a' + 10);
|
||||
else if (c >= 'A' && c <= 'F') d = (unsigned)(c - 'A' + 10);
|
||||
else return NULL;
|
||||
v = (v << 4) | d;
|
||||
}
|
||||
*out = v;
|
||||
return p + n;
|
||||
}
|
||||
|
||||
/* One region line of a flatview body, e.g.
|
||||
* " 0000000100000000-000000027fffffff (prio 0, ram): ram0 @0000000080000000 KVM"
|
||||
* Two leading spaces, 16-hex start, '-', 16-hex end, " (prio <N>, <flag>): <rest>".
|
||||
* Fills *start_gpa, *is_ram and, when present in <rest>, *file_off (with *has_off=1).
|
||||
* Returns 1 on a well-formed region line, 0 otherwise (not a region line for us). */
|
||||
typedef struct {
|
||||
uint64_t start_gpa;
|
||||
int is_ram; /* flag is exactly "ram" (not ramd/romd/rom/i/o/container) */
|
||||
int has_off; /* a "@<hex>" suffix was present in the descriptor */
|
||||
uint64_t file_off; /* value of that suffix */
|
||||
} region_line;
|
||||
|
||||
static int parse_region_line(const char* line, const char* nl, region_line* out) {
|
||||
/* leading " " then 16 hex, '-', 16 hex */
|
||||
if (line[0] != ' ' || line[1] != ' ') return 0;
|
||||
const char* p = line + 2;
|
||||
uint64_t start, end;
|
||||
p = parse_hexn(p, 16, &start);
|
||||
if (!p || *p != '-') return 0;
|
||||
p++;
|
||||
p = parse_hexn(p, 16, &end);
|
||||
if (!p) return 0;
|
||||
|
||||
/* " (prio <N>, <flag>):" — find the flag between ", " and ")". */
|
||||
if (strncmp(p, " (prio ", 7) != 0) return 0;
|
||||
const char* comma = memchr(p, ',', (size_t)(nl - p));
|
||||
if (!comma) return 0;
|
||||
const char* flag = comma + 1;
|
||||
while (flag < nl && *flag == ' ') flag++;
|
||||
const char* rparen = memchr(flag, ')', (size_t)(nl - flag));
|
||||
if (!rparen) return 0;
|
||||
size_t flen = (size_t)(rparen - flag);
|
||||
|
||||
out->start_gpa = start;
|
||||
out->is_ram = (flen == 3 && strncmp(flag, "ram", 3) == 0) ? 1 : 0;
|
||||
|
||||
/* optional "@<hex>" anywhere in the descriptor tail (after "): "). */
|
||||
out->has_off = 0;
|
||||
out->file_off = 0;
|
||||
const char* at = memchr(rparen, '@', (size_t)(nl - rparen));
|
||||
if (at) {
|
||||
char* stop = NULL;
|
||||
unsigned long long v = strtoull(at + 1, &stop, 16);
|
||||
if (stop && stop != at + 1) { out->has_off = 1; out->file_off = (uint64_t)v; }
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Locate the system flatview body: the lines AFTER " Root memory region: system" up to
|
||||
* the next "FlatView #" (or EOF). Returns the body start, sets *body_end; NULL if absent. */
|
||||
static const char* find_system_flatview(const char* text, const char** body_end) {
|
||||
const char* anchor = "Root memory region: system";
|
||||
const char* p = text;
|
||||
while ((p = strstr(p, anchor)) != NULL) {
|
||||
/* The root name must end the token (CR/LF/space/EOF) — reject "system.flash0" etc.,
|
||||
* and reject roots that merely contain the word elsewhere. QEMU's HMP output is
|
||||
* CRLF, so the byte after "system" is '\r'; accept it (LF-only input also works). */
|
||||
const char* after = p + strlen(anchor);
|
||||
if (*after == '\n' || *after == '\0' || *after == ' ' || *after == '\r') {
|
||||
const char* body = strchr(p, '\n');
|
||||
if (!body) return NULL;
|
||||
body++; /* first region line */
|
||||
const char* fv = strstr(body, "\nFlatView #");
|
||||
*body_end = fv ? fv + 1 : (body + strlen(body));
|
||||
return body;
|
||||
}
|
||||
p = after;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Primary signal: file offset (`@hex`) of the first ram region whose start GPA >= 4 GiB.
|
||||
* Returns 1 and sets *off when found, 0 otherwise. */
|
||||
static int high_ram_offset(const char* body, const char* end, uint64_t* off) {
|
||||
const char* p = body;
|
||||
while (p < end) {
|
||||
const char* nl = memchr(p, '\n', (size_t)(end - p));
|
||||
const char* line_end = nl ? nl : end;
|
||||
region_line r;
|
||||
if (parse_region_line(p, line_end, &r) &&
|
||||
r.is_ram && r.start_gpa >= RAM_HIGH_BASE && r.has_off) {
|
||||
*off = r.file_off;
|
||||
return 1;
|
||||
}
|
||||
if (!nl) break;
|
||||
p = nl + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Cross-validator / fallback: start GPA of the first non-ram region at or above the
|
||||
* PCI-hole base (the bottom of the 4 GiB hole == low for the classic layout). Returns 1
|
||||
* and sets *base when found, 0 otherwise. Blackhole holes below 0x80000000 are skipped
|
||||
* by the lower cutoff. */
|
||||
static int pci_hole_start(const char* body, const char* end, uint64_t* base) {
|
||||
const char* p = body;
|
||||
while (p < end) {
|
||||
const char* nl = memchr(p, '\n', (size_t)(end - p));
|
||||
const char* line_end = nl ? nl : end;
|
||||
region_line r;
|
||||
if (parse_region_line(p, line_end, &r) &&
|
||||
!r.is_ram && r.start_gpa >= PCI_HOLE_BASE && r.start_gpa < RAM_HIGH_BASE) {
|
||||
*base = r.start_gpa;
|
||||
return 1;
|
||||
}
|
||||
if (!nl) break;
|
||||
p = nl + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t mtree_low_split(const char* text) {
|
||||
if (!text) return 0;
|
||||
|
||||
const char* body_end = NULL;
|
||||
const char* body = find_system_flatview(text, &body_end);
|
||||
if (!body) return 0; /* no system AS => fail-closed */
|
||||
|
||||
uint64_t off = 0, base = 0;
|
||||
int have_off = high_ram_offset(body, body_end, &off);
|
||||
int have_base = pci_hole_start(body, body_end, &base);
|
||||
|
||||
if (have_off) {
|
||||
if (off == 0 || off == ~0ull) return 0; /* degenerate offset */
|
||||
/* cross-validate against the PCI-hole base when we have one */
|
||||
if (have_base && base != off) return 0; /* layout anomaly => fail-closed */
|
||||
return off; /* primary signal */
|
||||
}
|
||||
|
||||
/* No high-RAM (guest RAM all below 4 GiB): fall back to the PCI-hole base, but only
|
||||
* at or above the standard base so blackhole holes can never be mistaken for it. */
|
||||
if (have_base && base >= PCI_HOLE_BASE) return base;
|
||||
|
||||
return 0; /* nothing trustworthy */
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
* driver switches on (never on magic numbers). */
|
||||
|
||||
typedef enum {
|
||||
VMCTL_EV_ABS, VMCTL_EV_REL, VMCTL_EV_BTN, VMCTL_EV_KEY, VMCTL_EV_SCROLL
|
||||
VMCTL_EV_REL, VMCTL_EV_BTN, VMCTL_EV_KEY, VMCTL_EV_SCROLL
|
||||
} vmctl_ev_kind;
|
||||
|
||||
typedef struct {
|
||||
@@ -20,9 +20,10 @@ struct vmctl {
|
||||
vmctl_driver_ops ops;
|
||||
vmctl_driver driver;
|
||||
qmp_conn* qmp; /* control channel; NULL if none */
|
||||
int ui_fd_a; /* uinput driver: device A; -1 for QMP */
|
||||
int ui_fd_b; /* uinput driver: device B (BOTH); -1 */
|
||||
int ptr_mode; /* uinput driver: VMCTL_PTR_*; 0 for QMP */
|
||||
int ui_fd_a; /* uinput driver: device A (keyboard); -1 for QMP */
|
||||
int ui_fd_b; /* uinput driver: device B (relative mouse); -1 */
|
||||
char ui_evdev_a[64]; /* uinput driver: /dev/input/eventN of A ("" if none) */
|
||||
char ui_evdev_b[64]; /* uinput driver: /dev/input/eventN of B ("" if none) */
|
||||
|
||||
/* Held-state receipt: key/btn down-bits as THIS handle last actuated them
|
||||
* (not guest truth). Written only after a successful send in
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#ifndef VMCTL_UINPUT_LAYOUT_H
|
||||
#define VMCTL_UINPUT_LAYOUT_H
|
||||
#include "vmctl.h"
|
||||
|
||||
/* uinput_layout.h — DECLARATIVE capability split for the uinput driver, kept pure (no ioctl)
|
||||
* so it is unit-testable without /dev/uinput. The roles are passed into the driver as DATA, not
|
||||
* inferred as a side effect of one another; the hot path's button/wheel carrier follows the same
|
||||
* rule.
|
||||
*
|
||||
* Layout is CONSTANT (no absolute pointer): device A = keyboard only; device B = relative pointer
|
||||
* + mouse buttons + scroll wheel. Buttons + wheel ride device B (the relative-pointer carrier). */
|
||||
|
||||
typedef struct {
|
||||
int present; /* 1 if this device is created */
|
||||
int rel_motion; /* advertise relative X/Y (no device advertises abs) */
|
||||
int want_keyboard; /* advertise the keyboard keymap */
|
||||
int want_buttons; /* advertise the 8 mouse buttons */
|
||||
int want_wheel; /* advertise REL_WHEEL / REL_HWHEEL */
|
||||
} uinput_role;
|
||||
|
||||
/* Fill role_a/role_b with the constant layout. *btn_on_b is always 1: the button/wheel carrier
|
||||
* on the hot path is device B (the relative-pointer device). Both devices are always present. */
|
||||
static inline void vmctl_uinput_layout(uinput_role* role_a, uinput_role* role_b, int* btn_on_b) {
|
||||
role_a->present = 1;
|
||||
role_a->rel_motion = 0; /* keyboard-only: no pointer on A */
|
||||
role_a->want_keyboard = 1;
|
||||
role_a->want_buttons = 0;
|
||||
role_a->want_wheel = 0;
|
||||
|
||||
role_b->present = 1;
|
||||
role_b->rel_motion = 1;
|
||||
role_b->want_keyboard = 0;
|
||||
role_b->want_buttons = 1;
|
||||
role_b->want_wheel = 1;
|
||||
|
||||
if (btn_on_b) *btn_on_b = 1;
|
||||
}
|
||||
|
||||
#endif /* VMCTL_UINPUT_LAYOUT_H */
|
||||
@@ -9,12 +9,19 @@
|
||||
* (device_add) and undone at close (device_del). It is NOT a per-event
|
||||
* mechanism and lives entirely in the hotplug helpers below.
|
||||
*
|
||||
* uinput != virtio. Without qmp_path/input_bus the uinput device is created
|
||||
* orphaned (an external layer may forward it). The driver switches on
|
||||
* vmctl_ev_kind (never on magic numbers). */
|
||||
* uinput != virtio. The created uinput evdev nodes are forwarded into the guest
|
||||
* by an input-linux QMP object that the signaling vmhost seam adds over its own
|
||||
* connection (the evdev paths are exported via vmctl_uinput_evdev). The driver
|
||||
* switches on vmctl_ev_kind (never on magic numbers).
|
||||
*
|
||||
* Capability layout (constant, no absolute pointer): device A = keyboard only;
|
||||
* device B = relative pointer + mouse buttons + scroll wheel. Buttons/wheel ride
|
||||
* device B (the relative-pointer carrier). This split is DECLARATIVE: the roles
|
||||
* (want_buttons/want_wheel/rel_motion) are passed into uinput_create as data. */
|
||||
|
||||
#include "driver.h"
|
||||
#include "keymap.h"
|
||||
#include "uinput_layout.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
@@ -22,6 +29,7 @@
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <linux/uinput.h>
|
||||
#include <linux/input-event-codes.h>
|
||||
@@ -50,41 +58,35 @@ static void emit(int fd, uint16_t type, uint16_t code, int32_t val) {
|
||||
|
||||
static void syn(int fd) { emit(fd, EV_SYN, SYN_REPORT, 0); }
|
||||
|
||||
static int uinput_create(int rel_motion, const vmctl_uinput_id* id, const char* name, char evdev[64]) {
|
||||
/* The declarative per-device role (uinput_role) and the constant A/B split live in
|
||||
* uinput_layout.h so the layout is unit-testable without /dev/uinput. */
|
||||
static int uinput_create(const uinput_role* role, const vmctl_uinput_id* id,
|
||||
const char* name, char evdev[64]) {
|
||||
int fd = open("/dev/uinput", O_RDWR | O_CLOEXEC);
|
||||
if (fd < 0) return -1;
|
||||
|
||||
ioctl(fd, UI_SET_EVBIT, EV_SYN);
|
||||
|
||||
ioctl(fd, UI_SET_EVBIT, EV_KEY);
|
||||
/* Keyboard keybits come from the single source of truth: every key in
|
||||
* VMCTL_KEYS, so a key in the table always works through uinput too. */
|
||||
for (int i = 0; i < VMCTL_KEYS_LEN; i++)
|
||||
ioctl(fd, UI_SET_KEYBIT, VMCTL_KEYS[i].evdev);
|
||||
for (int b = 0; b < 8; b++)
|
||||
ioctl(fd, UI_SET_KEYBIT, (int)BTN_CODES[b]);
|
||||
|
||||
ioctl(fd, UI_SET_EVBIT, EV_REL);
|
||||
ioctl(fd, UI_SET_RELBIT, REL_WHEEL);
|
||||
ioctl(fd, UI_SET_RELBIT, REL_HWHEEL);
|
||||
if (rel_motion) {
|
||||
ioctl(fd, UI_SET_RELBIT, REL_X);
|
||||
ioctl(fd, UI_SET_RELBIT, REL_Y);
|
||||
if (role->want_keyboard) {
|
||||
/* Keyboard keybits come from the single source of truth: every key in
|
||||
* VMCTL_KEYS, so a key in the table always works through uinput too. */
|
||||
for (int i = 0; i < VMCTL_KEYS_LEN; i++)
|
||||
ioctl(fd, UI_SET_KEYBIT, VMCTL_KEYS[i].evdev);
|
||||
}
|
||||
if (role->want_buttons) {
|
||||
for (int b = 0; b < 8; b++)
|
||||
ioctl(fd, UI_SET_KEYBIT, (int)BTN_CODES[b]);
|
||||
}
|
||||
|
||||
if (!rel_motion) {
|
||||
ioctl(fd, UI_SET_EVBIT, EV_ABS);
|
||||
ioctl(fd, UI_SET_ABSBIT, ABS_X);
|
||||
ioctl(fd, UI_SET_ABSBIT, ABS_Y);
|
||||
|
||||
struct uinput_abs_setup ax;
|
||||
memset(&ax, 0, sizeof ax);
|
||||
ax.code = ABS_X;
|
||||
ax.absinfo.minimum = 0;
|
||||
ax.absinfo.maximum = VMCTL_ABS_MAX;
|
||||
ioctl(fd, UI_ABS_SETUP, &ax);
|
||||
ax.code = ABS_Y;
|
||||
ioctl(fd, UI_ABS_SETUP, &ax);
|
||||
if (role->want_wheel || role->rel_motion) ioctl(fd, UI_SET_EVBIT, EV_REL);
|
||||
if (role->want_wheel) {
|
||||
ioctl(fd, UI_SET_RELBIT, REL_WHEEL);
|
||||
ioctl(fd, UI_SET_RELBIT, REL_HWHEEL);
|
||||
}
|
||||
if (role->rel_motion) {
|
||||
ioctl(fd, UI_SET_RELBIT, REL_X);
|
||||
ioctl(fd, UI_SET_RELBIT, REL_Y);
|
||||
}
|
||||
|
||||
struct uinput_setup us;
|
||||
@@ -102,8 +104,28 @@ static int uinput_create(int rel_motion, const vmctl_uinput_id* id, const char*
|
||||
|
||||
char sysname[64] = {0};
|
||||
evdev[0] = '\0';
|
||||
if (ioctl(fd, UI_GET_SYSNAME(sizeof sysname), sysname) >= 0)
|
||||
snprintf(evdev, 64, "/dev/input/%s", sysname);
|
||||
/* UI_GET_SYSNAME returns the input-class directory name (e.g. "input174"), NOT the usable
|
||||
* device node: a bare /dev/input/<sysname> does not exist (QEMU input-linux fails to open
|
||||
* it). The evdev character device is /dev/input/eventN, exposed as the "event*" child of
|
||||
* /sys/class/input/<sysname>/ — resolve it there. */
|
||||
if (ioctl(fd, UI_GET_SYSNAME(sizeof sysname), sysname) >= 0) {
|
||||
char dpath[128];
|
||||
snprintf(dpath, sizeof dpath, "/sys/class/input/%s", sysname);
|
||||
DIR* dir = opendir(dpath);
|
||||
if (dir) {
|
||||
struct dirent* de;
|
||||
while ((de = readdir(dir)) != NULL)
|
||||
if (strncmp(de->d_name, "event", 5) == 0) {
|
||||
size_t nl = strlen(de->d_name);
|
||||
if (nl + 11 < 64) { /* "/dev/input/" is 11 chars + name + NUL */
|
||||
memcpy(evdev, "/dev/input/", 11);
|
||||
memcpy(evdev + 11, de->d_name, nl + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
}
|
||||
|
||||
if (!evdev[0]) {
|
||||
ioctl(fd, UI_DEV_DESTROY);
|
||||
@@ -144,7 +166,10 @@ static void qmp_unplug(qmp_conn* qmp, const char* id) {
|
||||
static int uinput_driver_send(vmctl_t* v, const vmctl_batch* b) {
|
||||
int fd_a = v->ui_fd_a;
|
||||
int fd_b = v->ui_fd_b;
|
||||
int both = (fd_b >= 0);
|
||||
/* Relative motion, mouse buttons and the scroll wheel all ride device B (the relative-pointer
|
||||
* carrier), matching the constant layout used at create time; the keyboard rides device A. */
|
||||
int fd_rel = fd_b;
|
||||
int fd_btn = fd_b;
|
||||
|
||||
for (int i = 0; i < b->count; i++) {
|
||||
int code = b->ev[i].code;
|
||||
@@ -152,30 +177,23 @@ static int uinput_driver_send(vmctl_t* v, const vmctl_batch* b) {
|
||||
double scl = b->ev[i].scroll;
|
||||
|
||||
switch ((vmctl_ev_kind)b->ev[i].kind) {
|
||||
case VMCTL_EV_ABS:
|
||||
if (v->ptr_mode == VMCTL_PTR_REL) return -1;
|
||||
emit(fd_a, EV_ABS, code == VMCTL_AXIS_X ? ABS_X : ABS_Y, value);
|
||||
syn(fd_a);
|
||||
break;
|
||||
case VMCTL_EV_REL: {
|
||||
if (!both && v->ptr_mode == VMCTL_PTR_ABS) return -1;
|
||||
int fd = both ? fd_b : fd_a;
|
||||
emit(fd, EV_REL, code == VMCTL_AXIS_X ? REL_X : REL_Y, value);
|
||||
syn(fd);
|
||||
emit(fd_rel, EV_REL, code == VMCTL_AXIS_X ? REL_X : REL_Y, value);
|
||||
syn(fd_rel);
|
||||
break;
|
||||
}
|
||||
case VMCTL_EV_BTN:
|
||||
if (code < 0 || code >= 8) return -1;
|
||||
emit(fd_a, EV_KEY, BTN_CODES[code], value);
|
||||
syn(fd_a);
|
||||
emit(fd_btn, EV_KEY, BTN_CODES[code], value);
|
||||
syn(fd_btn);
|
||||
break;
|
||||
case VMCTL_EV_KEY:
|
||||
emit(fd_a, EV_KEY, (uint16_t)code, value);
|
||||
syn(fd_a);
|
||||
break;
|
||||
case VMCTL_EV_SCROLL:
|
||||
emit(fd_a, EV_REL, code == VMCTL_SCROLL_V ? REL_WHEEL : REL_HWHEEL, (int32_t)scl);
|
||||
syn(fd_a);
|
||||
emit(fd_btn, EV_REL, code == VMCTL_SCROLL_V ? REL_WHEEL : REL_HWHEEL, (int32_t)scl);
|
||||
syn(fd_btn);
|
||||
break;
|
||||
default:
|
||||
return -1;
|
||||
@@ -212,13 +230,13 @@ vmctl_t* vmctl_open_uinput_driver(const vmctl_config* cfg) {
|
||||
const char* base = (cfg->uinput_id && cfg->uinput_id->name && cfg->uinput_id->name[0])
|
||||
? cfg->uinput_id->name : NULL;
|
||||
|
||||
/* A/B suffix is added by the library only when two devices are created
|
||||
* (VMCTL_PTR_BOTH) and only over a caller-supplied base name. */
|
||||
/* Two devices are always created (A=keyboard, B=relative mouse); the A/B suffix is added by
|
||||
* the library over a caller-supplied base name. */
|
||||
char name_a[UINPUT_MAX_NAME_SIZE];
|
||||
char name_b[UINPUT_MAX_NAME_SIZE];
|
||||
const char* dev_a = base ? base : HWID_NAME_A;
|
||||
const char* dev_b = HWID_NAME_B;
|
||||
if (cfg->ptr_mode == VMCTL_PTR_BOTH && base) {
|
||||
if (base) {
|
||||
int base_max = (int)(sizeof name_a - 1 /*NUL*/ - 2 /*"-A"*/);
|
||||
snprintf(name_a, sizeof name_a, "%.*s-A", base_max, base);
|
||||
snprintf(name_b, sizeof name_b, "%.*s-B", base_max, base);
|
||||
@@ -227,18 +245,22 @@ vmctl_t* vmctl_open_uinput_driver(const vmctl_config* cfg) {
|
||||
}
|
||||
|
||||
char evdev_a[64], evdev_b[64];
|
||||
int rel_a = (cfg->ptr_mode == VMCTL_PTR_REL);
|
||||
v->ui_fd_a = uinput_create(rel_a, id, dev_a, evdev_a);
|
||||
if (v->ui_fd_a < 0) { free(v); return NULL; }
|
||||
uinput_role role_a, role_b;
|
||||
vmctl_uinput_layout(&role_a, &role_b, NULL); /* constant A/B split */
|
||||
|
||||
if (cfg->ptr_mode == VMCTL_PTR_BOTH) {
|
||||
v->ui_fd_b = uinput_create(1, id, dev_b, evdev_b);
|
||||
v->ui_fd_a = uinput_create(&role_a, id, dev_a, evdev_a);
|
||||
if (v->ui_fd_a < 0) { free(v); return NULL; }
|
||||
memcpy(v->ui_evdev_a, evdev_a, sizeof v->ui_evdev_a);
|
||||
|
||||
if (role_b.present) {
|
||||
v->ui_fd_b = uinput_create(&role_b, id, dev_b, evdev_b);
|
||||
if (v->ui_fd_b < 0) {
|
||||
ioctl(v->ui_fd_a, UI_DEV_DESTROY);
|
||||
close(v->ui_fd_a);
|
||||
free(v);
|
||||
return NULL;
|
||||
}
|
||||
memcpy(v->ui_evdev_b, evdev_b, sizeof v->ui_evdev_b);
|
||||
}
|
||||
|
||||
if (cfg->qmp_path) {
|
||||
@@ -256,19 +278,16 @@ vmctl_t* vmctl_open_uinput_driver(const vmctl_config* cfg) {
|
||||
free(v);
|
||||
return NULL;
|
||||
}
|
||||
if (cfg->ptr_mode == VMCTL_PTR_BOTH) {
|
||||
if (qmp_plug(v->qmp, cfg->input_bus, evdev_b, PLUG_ID_B) < 0) {
|
||||
qmp_unplug(v->qmp, PLUG_ID_A);
|
||||
uinput_driver_close(v);
|
||||
free(v);
|
||||
return NULL;
|
||||
}
|
||||
if (qmp_plug(v->qmp, cfg->input_bus, evdev_b, PLUG_ID_B) < 0) {
|
||||
qmp_unplug(v->qmp, PLUG_ID_A);
|
||||
uinput_driver_close(v);
|
||||
free(v);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v->ops.send = uinput_driver_send;
|
||||
v->ops.close = uinput_driver_close;
|
||||
v->ptr_mode = cfg->ptr_mode;
|
||||
return v;
|
||||
}
|
||||
|
||||
+12
-15
@@ -29,12 +29,6 @@ void vmctl_batch_init(vmctl_batch* b) {
|
||||
b->count = 0;
|
||||
}
|
||||
|
||||
void vmctl_batch_abs(vmctl_batch* b, int axis, int value) {
|
||||
if (b->count >= VMCTL_BATCH_MAX) return;
|
||||
vmctl_event* e = &b->ev[b->count++];
|
||||
e->kind = VMCTL_EV_ABS; e->code = axis; e->value = value; e->scroll = 0.0;
|
||||
}
|
||||
|
||||
void vmctl_batch_rel(vmctl_batch* b, int axis, int delta) {
|
||||
if (b->count >= VMCTL_BATCH_MAX) return;
|
||||
vmctl_event* e = &b->ev[b->count++];
|
||||
@@ -65,7 +59,7 @@ int vmctl_batch_send(vmctl_t* v, vmctl_batch* b) {
|
||||
if (rc != 0) return rc; /* not sent = not recorded; never touch the receipt */
|
||||
|
||||
/* Record the actuated key/btn down-bits (write-only; the send path above
|
||||
* never reads this map). abs/rel/scroll have no held state. */
|
||||
* never reads this map). rel/scroll have no held state. */
|
||||
for (int i = 0; i < b->count; i++) {
|
||||
const vmctl_event* e = &b->ev[i];
|
||||
int down = e->value ? 1 : 0;
|
||||
@@ -86,7 +80,7 @@ int vmctl_batch_send(vmctl_t* v, vmctl_batch* b) {
|
||||
else v->btns_held &= ~mask;
|
||||
break;
|
||||
}
|
||||
default: break; /* abs/rel/scroll: no-op for receipt */
|
||||
default: break; /* rel/scroll: no-op for receipt */
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
@@ -94,13 +88,6 @@ int vmctl_batch_send(vmctl_t* v, vmctl_batch* b) {
|
||||
|
||||
/* ===== Single-event wrappers ===== */
|
||||
|
||||
int vmctl_abs(vmctl_t* v, int axis, int value) {
|
||||
vmctl_batch b;
|
||||
vmctl_batch_init(&b);
|
||||
vmctl_batch_abs(&b, axis, value);
|
||||
return vmctl_batch_send(v, &b);
|
||||
}
|
||||
|
||||
int vmctl_rel(vmctl_t* v, int axis, int delta) {
|
||||
vmctl_batch b;
|
||||
vmctl_batch_init(&b);
|
||||
@@ -154,3 +141,13 @@ unsigned vmctl_btns_snapshot(vmctl_t* v) {
|
||||
if (!v) return 0;
|
||||
return v->btns_held;
|
||||
}
|
||||
|
||||
/* ===== uinput evdev export (UINPUT-only) ===== */
|
||||
|
||||
int vmctl_uinput_evdev(vmctl_t* v, char a[64], char b[64]) {
|
||||
if (!v || v->driver != VMCTL_DRIVER_UINPUT) return -1;
|
||||
int n = 0;
|
||||
if (a) { memcpy(a, v->ui_evdev_a, sizeof v->ui_evdev_a); if (a[0]) n++; }
|
||||
if (b) { memcpy(b, v->ui_evdev_b, sizeof v->ui_evdev_b); if (b[0]) n++; }
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -29,11 +29,6 @@ static int qmp_driver_send(vmctl_t* v, const vmctl_batch* b) {
|
||||
double scl = b->ev[i].scroll;
|
||||
|
||||
switch ((vmctl_ev_kind)b->ev[i].kind) {
|
||||
case VMCTL_EV_ABS:
|
||||
pos += snprintf(json + pos, (int)sizeof json - pos,
|
||||
"{\"type\":\"abs\",\"data\":{\"axis\":\"%s\",\"value\":%d}}",
|
||||
code == VMCTL_AXIS_X ? "x" : "y", value);
|
||||
break;
|
||||
case VMCTL_EV_REL:
|
||||
pos += snprintf(json + pos, (int)sizeof json - pos,
|
||||
"{\"type\":\"rel\",\"data\":{\"axis\":\"%s\",\"value\":%d}}",
|
||||
@@ -87,7 +82,6 @@ vmctl_t* vmctl_open_qmp_driver(const vmctl_config* cfg) {
|
||||
v->qmp = qmp;
|
||||
v->ui_fd_a = -1;
|
||||
v->ui_fd_b = -1;
|
||||
v->ptr_mode = 0;
|
||||
v->ops.send = qmp_driver_send;
|
||||
v->ops.close = qmp_driver_close;
|
||||
return v;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef VGPU_ATOMIC_SHIM_H
|
||||
#define VGPU_ATOMIC_SHIM_H
|
||||
|
||||
/* atomic-shim.h — x86-TSO memory-order accessors (arch, not OS).
|
||||
*
|
||||
* x86-TSO memory-order shim. NO _Atomic in the shared region type: the consumer
|
||||
* maps the region as raw bytes. Synchronization lives entirely in the producer's
|
||||
* accessors here. Per-compiler implementation, never exposed in the contract.
|
||||
*
|
||||
* On x86_64 every naturally-aligned MOV up to 8 bytes is atomic and stores are
|
||||
* already release / loads already acquire at the hardware level; the only things
|
||||
* we must prevent are (1) compiler reordering across the sync point and
|
||||
* (2) store-buffer visibility delay between the data writes and the publish
|
||||
* store, for which an explicit SFENCE is used at publish boundaries.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
|
||||
#include <intrin.h>
|
||||
|
||||
static inline void vgpu_compiler_barrier(void) { _ReadWriteBarrier(); }
|
||||
static inline void vgpu_sfence(void) { _mm_sfence(); }
|
||||
|
||||
static inline void vgpu_store_release32(volatile uint32_t* p, uint32_t v) {
|
||||
_ReadWriteBarrier();
|
||||
*p = v;
|
||||
}
|
||||
|
||||
static inline uint32_t vgpu_load_acquire32(const volatile uint32_t* p) {
|
||||
uint32_t v = *p;
|
||||
_ReadWriteBarrier();
|
||||
return v;
|
||||
}
|
||||
|
||||
#else /* gcc / mingw / clang */
|
||||
|
||||
static inline void vgpu_compiler_barrier(void) { __asm__ __volatile__("" ::: "memory"); }
|
||||
static inline void vgpu_sfence(void) { __asm__ __volatile__("sfence" ::: "memory"); }
|
||||
|
||||
static inline void vgpu_store_release32(volatile uint32_t* p, uint32_t v) {
|
||||
__atomic_store_n(p, v, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
static inline uint32_t vgpu_load_acquire32(const volatile uint32_t* p) {
|
||||
return __atomic_load_n(p, __ATOMIC_ACQUIRE);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif /* VGPU_ATOMIC_SHIM_H */
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef VGPU_CAPTURE_H
|
||||
#define VGPU_CAPTURE_H
|
||||
|
||||
/* capture.h — extension seam for capture backends.
|
||||
* A backend produces desktop frames and submits them to the presenter. This
|
||||
* header is OS-agnostic: it names backends through an opaque vgpu_ctx* and a
|
||||
* uniform start contract. A platform layer defines vgpu_ctx and any private
|
||||
* backend plumbing (see src/stream/win32/capture-win32.h). A future Linux layer
|
||||
* implements the same seam against its own vgpu_ctx + region/sync/clock. */
|
||||
|
||||
/* Opaque runtime context, defined by the platform layer (win32: ctx.h). */
|
||||
typedef struct vgpu_ctx vgpu_ctx;
|
||||
|
||||
/* Start a capture backend. Returns 1 on success; on success the backend has
|
||||
* spawned its capture thread(s) (which received ctx) and set ctx->backend /
|
||||
* ctx->draw_cursor_cap. The submit contract: each captured desktop frame is
|
||||
* handed to the presenter via vgpu_present_submit(). */
|
||||
typedef int (*capture_start_fn)(vgpu_ctx* ctx, int fps);
|
||||
|
||||
typedef struct {
|
||||
const char* name;
|
||||
capture_start_fn start;
|
||||
} capture_backend;
|
||||
|
||||
/* Data-driven backend table; the entry point selects by env or availability. */
|
||||
const capture_backend* capture_backends(int* count);
|
||||
|
||||
#endif /* VGPU_CAPTURE_H */
|
||||
@@ -0,0 +1,88 @@
|
||||
#ifndef VGPU_STREAM_ENGINE_H
|
||||
#define VGPU_STREAM_ENGINE_H
|
||||
|
||||
/* stream.h — OS-agnostic streaming protocol over the shared contract.
|
||||
* Declares the neutral region-view handle (resolved contract pointers) and the
|
||||
* seqlock publish / control-reconcile API. No platform headers: the engine
|
||||
* operates purely on the contract; a platform layer (e.g. src/stream/win32/)
|
||||
* builds the region and hands its pointers in as a vgpu_region_view. */
|
||||
|
||||
#include <stdint.h>
|
||||
#include "vgpu_stream.h" /* contract: producer/control types, slot geometry */
|
||||
|
||||
/* Neutral view of the live contract: the three resolved blocks the engine
|
||||
* publishes into / reconciles against. The platform region owns the backing
|
||||
* memory; this is a borrowed view (no ownership). */
|
||||
typedef struct {
|
||||
vgpu_producer_t* producer;
|
||||
vgpu_control_t* control;
|
||||
uint8_t* ring;
|
||||
} vgpu_region_view;
|
||||
|
||||
/* Resolved view of the control block after a clean generation read. */
|
||||
typedef struct {
|
||||
uint32_t gen; /* even generation that was read (for ctrl_ack) */
|
||||
uint32_t desired_state; /* VGPU_CMD_* */
|
||||
uint32_t target_fps;
|
||||
uint32_t draw_cursor;
|
||||
uint32_t full_frame_req;
|
||||
uint32_t consumer_tick;
|
||||
uint32_t attached;
|
||||
} vgpu_control_view;
|
||||
|
||||
/* Seqlock-publish a tight BGRA frame into the next ring slot.
|
||||
* Clamps by SLOT_STRIDE (rejects frames that do not fit). Writes desc[],
|
||||
* bumps frame_id, release-stores latest. Returns 0 on publish, 1 if dropped
|
||||
* (frame too large for a slot). */
|
||||
int vgpu_publish_frame(const vgpu_region_view* rv, const uint8_t* tight_bgra,
|
||||
uint32_t width, uint32_t height, uint64_t timestamp_ns);
|
||||
|
||||
/* Read control block under its generation seqlock (bounded retry). Returns 1
|
||||
* on a clean read (view filled), 0 if the writer kept it busy past the limit. */
|
||||
int vgpu_control_read(const vgpu_region_view* rv, vgpu_control_view* out);
|
||||
|
||||
/* Echo the applied generation back to the host. */
|
||||
void vgpu_publish_ctrl_ack(const vgpu_region_view* rv, uint32_t gen);
|
||||
|
||||
/* Status / lifecycle helpers (cold line). */
|
||||
void vgpu_set_status(const vgpu_region_view* rv, uint32_t status);
|
||||
void vgpu_set_backend(const vgpu_region_view* rv, uint32_t backend);
|
||||
void vgpu_set_error(const vgpu_region_view* rv, uint32_t error_code);
|
||||
void vgpu_set_applied_fps(const vgpu_region_view* rv, uint32_t fps);
|
||||
void vgpu_bump_run_epoch(const vgpu_region_view* rv);
|
||||
void vgpu_tick_heartbeat(const vgpu_region_view* rv);
|
||||
void vgpu_publish_full_frame_ack(const vgpu_region_view* rv, uint32_t req);
|
||||
|
||||
/* Publish the on-screen cursor position (host-RO). Position is sensor data and is
|
||||
* reported independent of control.draw_cursor (host may draw its own overlay even when the
|
||||
* producer does not composite the cursor). x,y are screen coords (signed; multi-monitor may
|
||||
* be negative); visible!=0 when the cursor is shown. Packs x|y into one 8-aligned 64-bit
|
||||
* field (single atomic store) and bumps cursor_seq last. */
|
||||
void vgpu_publish_cursor(const vgpu_region_view* rv, int32_t x, int32_t y, uint32_t visible);
|
||||
|
||||
/* Publish Tier-1 cursor shape data (host-RO), written under the same cursor_seq gate as
|
||||
* vgpu_publish_cursor: call this BEFORE vgpu_publish_cursor so the position publish bumps
|
||||
* cursor_seq last and gates the whole cursor line consistently. hot_x/hot_y are the glyph
|
||||
* hotspot; gw/gh are glyph dims; cursor_id is a VGPU_CURSOR_ID_* shape identity. */
|
||||
void vgpu_publish_cursor_shape(const vgpu_region_view* rv,
|
||||
uint32_t hot_x, uint32_t hot_y,
|
||||
uint32_t gw, uint32_t gh, uint32_t cursor_id);
|
||||
|
||||
/* Publish the monotonic timestamp (ns) of the last scene-content change. Single 8-aligned
|
||||
* atomic store (heartbeat pattern). The producer reports the raw stamp only; the host derives
|
||||
* "ms idle" by subtracting from its own clock — no behavioural distillation in the producer. */
|
||||
void vgpu_publish_content_change(const vgpu_region_view* rv, uint64_t change_ns);
|
||||
|
||||
/* Publish display geometry under the geom_seq seqlock (odd/even, like the frame seqlock).
|
||||
* Sampled rarely (session start + reactive resample on desc-size delta / backend recreate),
|
||||
* read by the host with bounded retry. virt_* is the virtual-desktop bbox (interprets negative
|
||||
* cursor_pos); cap_x/cap_y is the captured output's origin in virtual-desktop coords (the
|
||||
* captured surface SIZE comes from desc.width/height, not from here). dpi/refresh_mhz describe
|
||||
* the captured output (96=100% / milli-Hz; 0=unknown). */
|
||||
void vgpu_publish_geometry(const vgpu_region_view* rv,
|
||||
int32_t virt_x, int32_t virt_y,
|
||||
uint32_t virt_w, uint32_t virt_h,
|
||||
int32_t cap_x, int32_t cap_y,
|
||||
uint32_t dpi, uint32_t refresh_mhz);
|
||||
|
||||
#endif /* VGPU_STREAM_ENGINE_H */
|
||||
@@ -0,0 +1,163 @@
|
||||
/* publish.c — OS-agnostic implementation of the streaming protocol.
|
||||
* Operates purely on the contract through a borrowed vgpu_region_view; no
|
||||
* platform headers, no runtime context. The x86-TSO ordering lives in the
|
||||
* atomic shim. */
|
||||
|
||||
#include <string.h>
|
||||
#include "vgpu_stream.h" /* contract types / slot geometry */
|
||||
#include "atomic-shim.h" /* x86-TSO memory-order accessors */
|
||||
#include "stream.h" /* region-view handle + this API */
|
||||
|
||||
#define VGPU_CTRL_READ_TRIES 16u
|
||||
|
||||
int vgpu_publish_frame(const vgpu_region_view* rv, const uint8_t* tight_bgra,
|
||||
uint32_t width, uint32_t height, uint64_t timestamp_ns) {
|
||||
vgpu_producer_t* p = rv->producer;
|
||||
|
||||
const uint32_t stride = width * 4u; /* tight invariant */
|
||||
const uint64_t need = (uint64_t)height * stride;
|
||||
if (need > VGPU_SLOT_STRIDE) /* clamp by slot size */
|
||||
return 1;
|
||||
|
||||
uint32_t cur = vgpu_load_acquire32(&p->latest);
|
||||
uint32_t S = (cur == VGPU_LATEST_NONE) ? 0u : ((cur + 1u) % VGPU_SLOT_COUNT);
|
||||
|
||||
uint8_t* dst = rv->ring + (size_t)S * VGPU_SLOT_STRIDE;
|
||||
|
||||
/* seqlock: even -> odd (writing) */
|
||||
vgpu_store_release32(&p->seq[S], p->seq[S] + 1u);
|
||||
vgpu_compiler_barrier();
|
||||
|
||||
/* descriptor (self-describing slot) */
|
||||
p->desc[S].width = width;
|
||||
p->desc[S].height = height;
|
||||
p->desc[S].stride = stride;
|
||||
p->desc[S].format = VGPU_FMT_BGRA8888;
|
||||
p->desc[S].frame_id = p->frame_id + 1u;
|
||||
p->desc[S].timestamp_ns = timestamp_ns;
|
||||
|
||||
/* pixels (source is already tight) */
|
||||
memcpy(dst, tight_bgra, (size_t)need);
|
||||
|
||||
vgpu_sfence();
|
||||
/* seqlock: odd -> even (stable) */
|
||||
vgpu_store_release32(&p->seq[S], p->seq[S] + 1u);
|
||||
vgpu_sfence();
|
||||
|
||||
p->frame_id += 1u;
|
||||
vgpu_store_release32(&p->latest, S);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int vgpu_control_read(const vgpu_region_view* rv, vgpu_control_view* out) {
|
||||
volatile vgpu_control_t* c = rv->control;
|
||||
|
||||
for (uint32_t t = 0; t < VGPU_CTRL_READ_TRIES; t++) {
|
||||
uint32_t g0 = vgpu_load_acquire32(&c->ctrl_gen);
|
||||
if (g0 & 1u)
|
||||
continue; /* writer in progress */
|
||||
vgpu_compiler_barrier();
|
||||
|
||||
uint32_t desired = c->desired_state;
|
||||
uint32_t fps = c->target_fps;
|
||||
uint32_t cursor = c->draw_cursor;
|
||||
uint32_t ffreq = c->full_frame_req;
|
||||
uint32_t ctick = c->consumer_tick;
|
||||
uint32_t att = c->attached;
|
||||
|
||||
vgpu_compiler_barrier();
|
||||
uint32_t g1 = vgpu_load_acquire32(&c->ctrl_gen);
|
||||
if (g0 != g1)
|
||||
continue; /* torn read, retry */
|
||||
|
||||
out->gen = g0;
|
||||
out->desired_state = desired;
|
||||
out->target_fps = fps;
|
||||
out->draw_cursor = cursor;
|
||||
out->full_frame_req = ffreq;
|
||||
out->consumer_tick = ctick;
|
||||
out->attached = att;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void vgpu_publish_ctrl_ack(const vgpu_region_view* rv, uint32_t gen) {
|
||||
vgpu_store_release32(&rv->producer->ctrl_ack, gen);
|
||||
}
|
||||
|
||||
void vgpu_set_status(const vgpu_region_view* rv, uint32_t status) {
|
||||
vgpu_store_release32(&rv->producer->status, status);
|
||||
}
|
||||
|
||||
void vgpu_set_backend(const vgpu_region_view* rv, uint32_t backend) {
|
||||
vgpu_store_release32(&rv->producer->backend, backend);
|
||||
}
|
||||
|
||||
void vgpu_set_error(const vgpu_region_view* rv, uint32_t error_code) {
|
||||
vgpu_store_release32(&rv->producer->error_code, error_code);
|
||||
}
|
||||
|
||||
void vgpu_set_applied_fps(const vgpu_region_view* rv, uint32_t fps) {
|
||||
vgpu_store_release32(&rv->producer->applied_fps, fps);
|
||||
}
|
||||
|
||||
void vgpu_bump_run_epoch(const vgpu_region_view* rv) {
|
||||
vgpu_producer_t* p = rv->producer;
|
||||
vgpu_store_release32(&p->run_epoch, p->run_epoch + 1u);
|
||||
}
|
||||
|
||||
void vgpu_tick_heartbeat(const vgpu_region_view* rv) {
|
||||
/* 64-bit aligned single MOV is atomic on x86_64; barrier orders it */
|
||||
rv->producer->heartbeat += 1u;
|
||||
vgpu_compiler_barrier();
|
||||
}
|
||||
|
||||
void vgpu_publish_full_frame_ack(const vgpu_region_view* rv, uint32_t req) {
|
||||
vgpu_store_release32(&rv->producer->full_frame_ack, req);
|
||||
}
|
||||
|
||||
void vgpu_publish_cursor(const vgpu_region_view* rv, int32_t x, int32_t y, uint32_t visible) {
|
||||
vgpu_producer_t* p = rv->producer;
|
||||
/* pack: low 32 = x, high 32 = y (signed → two's-complement bits) */
|
||||
uint64_t packed = ((uint64_t)(uint32_t)y << 32) | (uint64_t)(uint32_t)x;
|
||||
/* 64-bit aligned single MOV is atomic on x86_64; barrier orders it (heartbeat pattern) */
|
||||
p->cursor_pos = packed;
|
||||
vgpu_store_release32(&p->cursor_visible, visible);
|
||||
/* publish seq last: its release-store gates the pos/visible writes above for the host */
|
||||
vgpu_store_release32(&p->cursor_seq, p->cursor_seq + 1u);
|
||||
}
|
||||
|
||||
void vgpu_publish_cursor_shape(const vgpu_region_view* rv, uint32_t hot_x, uint32_t hot_y,
|
||||
uint32_t gw, uint32_t gh, uint32_t cursor_id) {
|
||||
vgpu_producer_t* p = rv->producer;
|
||||
/* pack 16|16 strictly unsigned (mask low half so no sign bits bleed into the high half).
|
||||
* No own seq: the following vgpu_publish_cursor bumps cursor_seq last and gates this line. */
|
||||
vgpu_store_release32(&p->cursor_hotspot, (hot_y << 16) | (hot_x & 0xFFFFu));
|
||||
vgpu_store_release32(&p->cursor_glyph, (gh << 16) | (gw & 0xFFFFu));
|
||||
vgpu_store_release32(&p->cursor_id, cursor_id);
|
||||
}
|
||||
|
||||
void vgpu_publish_content_change(const vgpu_region_view* rv, uint64_t change_ns) {
|
||||
/* 64-bit aligned single MOV is atomic on x86_64; barrier orders it (heartbeat pattern) */
|
||||
rv->producer->content_change_ns = change_ns;
|
||||
vgpu_compiler_barrier();
|
||||
}
|
||||
|
||||
void vgpu_publish_geometry(const vgpu_region_view* rv, int32_t virt_x, int32_t virt_y,
|
||||
uint32_t virt_w, uint32_t virt_h,
|
||||
int32_t cap_x, int32_t cap_y,
|
||||
uint32_t dpi, uint32_t refresh_mhz) {
|
||||
vgpu_producer_t* p = rv->producer;
|
||||
/* seqlock: even -> odd (writing) */
|
||||
vgpu_store_release32(&p->geom_seq, p->geom_seq + 1u);
|
||||
vgpu_compiler_barrier();
|
||||
p->virt_x = virt_x; p->virt_y = virt_y;
|
||||
p->virt_w = virt_w; p->virt_h = virt_h;
|
||||
p->cap_x = cap_x; p->cap_y = cap_y;
|
||||
p->dpi = dpi; p->refresh_mhz = refresh_mhz;
|
||||
vgpu_sfence();
|
||||
/* seqlock: odd -> even (stable) */
|
||||
vgpu_store_release32(&p->geom_seq, p->geom_seq + 1u);
|
||||
vgpu_sfence();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef VGPU_CAPTURE_WIN32_H
|
||||
#define VGPU_CAPTURE_WIN32_H
|
||||
|
||||
/* capture-win32.h — private win32 plumbing shared by the capture backends.
|
||||
* Not part of the OS-agnostic capture seam (see src/stream/include/capture.h):
|
||||
* it depends on the win32 vgpu_ctx and the thread-handoff convention. */
|
||||
|
||||
#include "ctx.h" /* win32 vgpu_ctx (full definition) */
|
||||
|
||||
/* Thread argument passed to capture threads via LPVOID. Heap-allocated by the
|
||||
* backend's *_start, owned and freed by the thread. Carries the explicit ctx
|
||||
* (no global state) plus per-backend state pointer. */
|
||||
typedef struct {
|
||||
vgpu_ctx* ctx;
|
||||
int fps;
|
||||
void* backend_state; /* opaque per-backend handle block */
|
||||
} capture_thread_arg;
|
||||
|
||||
#endif /* VGPU_CAPTURE_WIN32_H */
|
||||
@@ -0,0 +1,19 @@
|
||||
/* capture.c — win32 registration of the capture backends into the neutral
|
||||
* capture seam's backend table (data-driven; no per-backend branching). */
|
||||
|
||||
#include "capture.h" /* neutral seam: capture_backend / capture_backends */
|
||||
#include "capture_nvfbc.h"
|
||||
#include "capture_dda.h"
|
||||
#include "capture_gdi.h"
|
||||
|
||||
/* data-driven backend table; main selects by EYES env or first available */
|
||||
static const capture_backend g_backends[] = {
|
||||
{ "nvfbc", nvfbc_start },
|
||||
{ "dda", dda_start },
|
||||
{ "gdi", gdi_start },
|
||||
};
|
||||
|
||||
const capture_backend* capture_backends(int* count) {
|
||||
*count = (int)(sizeof g_backends / sizeof g_backends[0]);
|
||||
return g_backends;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define COBJMACROS
|
||||
#include <windows.h>
|
||||
#include <d3d11.h>
|
||||
#include <dxgi1_2.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "capture_dda.h"
|
||||
#include "capture-win32.h" /* capture_thread_arg (win32-private) */
|
||||
#include "present.h"
|
||||
#include "cursor.h" /* cursor_resolve_id + ctx->cursor compose state */
|
||||
#include "geometry.h" /* reactive geometry resample on recreate */
|
||||
#include "stream.h" /* vgpu_publish_cursor / vgpu_publish_cursor_shape */
|
||||
|
||||
typedef struct {
|
||||
ID3D11Device* dev;
|
||||
ID3D11DeviceContext* dctx;
|
||||
IDXGIOutput1* out1;
|
||||
IDXGIOutputDuplication* dup;
|
||||
ID3D11Texture2D* staging;
|
||||
UINT W, H;
|
||||
int32_t cap_x, cap_y; /* captured output origin (virt coords) */
|
||||
UINT64 last_mouse_update; /* shape-gate by fi.LastMouseUpdateTime */
|
||||
int seeded; /* cold-start position seed done */
|
||||
} dda_state;
|
||||
|
||||
/* Source the cursor from the already-fetched frame info (0 syscalls for position) and publish
|
||||
* it under the cursor_seq gate. Position/visibility come from fi.PointerPosition; the shape is
|
||||
* re-extracted only when fi.LastMouseUpdateTime changed (shape-gate). Cold start: fi is invalid
|
||||
* until the mouse first moves (LastMouseUpdateTime==0) — seed the position once via one
|
||||
* GetCursorInfo, then rely on fi. ctx->cursor compose fields are written under ctx->lock; the
|
||||
* producer-block publish uses release/seq, no lock. */
|
||||
static void dda_source_cursor(vgpu_ctx* ctx, dda_state* st,
|
||||
const DXGI_OUTDUPL_FRAME_INFO* fi) {
|
||||
int vis = fi->PointerPosition.Visible ? 1 : 0;
|
||||
int x, y;
|
||||
UINT64 upd = (UINT64)fi->LastMouseUpdateTime.QuadPart;
|
||||
|
||||
if (!st->seeded && upd == 0) {
|
||||
CURSORINFO ci; ci.cbSize = sizeof ci;
|
||||
if (GetCursorInfo(&ci)) {
|
||||
vis = (ci.flags & CURSOR_SHOWING) != 0;
|
||||
x = ci.ptScreenPos.x; y = ci.ptScreenPos.y;
|
||||
} else {
|
||||
x = ctx->cursor.x; y = ctx->cursor.y;
|
||||
}
|
||||
st->seeded = 1;
|
||||
} else {
|
||||
x = fi->PointerPosition.Position.x;
|
||||
y = fi->PointerPosition.Position.y;
|
||||
if (upd != 0) st->seeded = 1;
|
||||
}
|
||||
|
||||
/* shape-gate: re-extract only when the mouse-update stamp advanced */
|
||||
if (upd != 0 && upd != st->last_mouse_update) {
|
||||
CURSORINFO ci; ci.cbSize = sizeof ci;
|
||||
if (GetCursorInfo(&ci) && ci.hCursor && ci.hCursor != ctx->cursor.handle) {
|
||||
EnterCriticalSection(&ctx->lock);
|
||||
cursor_apply_shape(ctx, ci.hCursor);
|
||||
LeaveCriticalSection(&ctx->lock);
|
||||
}
|
||||
st->last_mouse_update = upd;
|
||||
}
|
||||
|
||||
EnterCriticalSection(&ctx->lock);
|
||||
ctx->cursor.visible = vis;
|
||||
ctx->cursor.x = x; ctx->cursor.y = y;
|
||||
uint32_t hx = (uint32_t)ctx->cursor.hot_x, hy = (uint32_t)ctx->cursor.hot_y;
|
||||
uint32_t gw = (uint32_t)ctx->cursor.gw, gh = (uint32_t)ctx->cursor.gh;
|
||||
uint32_t cid = (uint32_t)ctx->cursor.cursor_id;
|
||||
LeaveCriticalSection(&ctx->lock);
|
||||
|
||||
vgpu_publish_cursor_shape(&ctx->view, hx, hy, gw, gh, cid);
|
||||
vgpu_publish_cursor(&ctx->view, (int32_t)x, (int32_t)y, (uint32_t)vis);
|
||||
}
|
||||
|
||||
static DWORD WINAPI dda_thread(LPVOID param) {
|
||||
capture_thread_arg* arg = (capture_thread_arg*)param;
|
||||
vgpu_ctx* ctx = arg->ctx;
|
||||
dda_state* st = (dda_state*)arg->backend_state;
|
||||
free(arg);
|
||||
|
||||
for (;;) {
|
||||
DXGI_OUTDUPL_FRAME_INFO fi;
|
||||
IDXGIResource* res = NULL;
|
||||
HRESULT hr = st->dup->lpVtbl->AcquireNextFrame(st->dup, 1000, &fi, &res);
|
||||
if (hr == DXGI_ERROR_WAIT_TIMEOUT) continue;
|
||||
if (hr == DXGI_ERROR_ACCESS_LOST) {
|
||||
if (st->dup) { st->dup->lpVtbl->Release(st->dup); st->dup = NULL; }
|
||||
if (FAILED(st->out1->lpVtbl->DuplicateOutput(st->out1,
|
||||
(IUnknown*)st->dev, &st->dup))) {
|
||||
Sleep(200);
|
||||
} else {
|
||||
/* display config may have changed across the access loss → resample geometry */
|
||||
geometry_sample_and_publish(ctx, st->cap_x, st->cap_y);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (FAILED(hr)) { Sleep(50); continue; }
|
||||
|
||||
dda_source_cursor(ctx, st, &fi);
|
||||
|
||||
ID3D11Texture2D* tex = NULL;
|
||||
res->lpVtbl->QueryInterface(res, &IID_ID3D11Texture2D, (void**)&tex);
|
||||
if (tex) {
|
||||
st->dctx->lpVtbl->CopyResource(st->dctx,
|
||||
(ID3D11Resource*)st->staging, (ID3D11Resource*)tex);
|
||||
D3D11_MAPPED_SUBRESOURCE m;
|
||||
if (SUCCEEDED(st->dctx->lpVtbl->Map(st->dctx,
|
||||
(ID3D11Resource*)st->staging, 0, D3D11_MAP_READ, 0, &m))) {
|
||||
vgpu_present_submit(ctx, (const uint8_t*)m.pData, st->W, st->H, m.RowPitch);
|
||||
st->dctx->lpVtbl->Unmap(st->dctx, (ID3D11Resource*)st->staging, 0);
|
||||
}
|
||||
tex->lpVtbl->Release(tex);
|
||||
}
|
||||
if (res) res->lpVtbl->Release(res);
|
||||
st->dup->lpVtbl->ReleaseFrame(st->dup);
|
||||
}
|
||||
return 0; /* unreachable; satisfies -Wreturn-type */
|
||||
}
|
||||
|
||||
int dda_start(vgpu_ctx* ctx, int fps) {
|
||||
(void)fps;
|
||||
dda_state* st = (dda_state*)calloc(1, sizeof *st);
|
||||
if (!st) return 0;
|
||||
|
||||
D3D_FEATURE_LEVEL fl;
|
||||
if (FAILED(D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0, NULL, 0,
|
||||
D3D11_SDK_VERSION, &st->dev, &fl, &st->dctx))) {
|
||||
fprintf(stderr, "eyes(dda): D3D11CreateDevice failed\n");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
IDXGIDevice* dxgiDev = NULL;
|
||||
IDXGIAdapter* adapter = NULL;
|
||||
IDXGIOutput* output = NULL;
|
||||
st->dev->lpVtbl->QueryInterface(st->dev, &IID_IDXGIDevice, (void**)&dxgiDev);
|
||||
if (dxgiDev) dxgiDev->lpVtbl->GetAdapter(dxgiDev, &adapter);
|
||||
if (adapter) adapter->lpVtbl->EnumOutputs(adapter, 0, &output);
|
||||
if (output) {
|
||||
DXGI_OUTPUT_DESC od;
|
||||
if (SUCCEEDED(output->lpVtbl->GetDesc(output, &od))) {
|
||||
st->cap_x = (int32_t)od.DesktopCoordinates.left;
|
||||
st->cap_y = (int32_t)od.DesktopCoordinates.top;
|
||||
}
|
||||
output->lpVtbl->QueryInterface(output, &IID_IDXGIOutput1, (void**)&st->out1);
|
||||
}
|
||||
|
||||
if (output) output->lpVtbl->Release(output);
|
||||
if (adapter) adapter->lpVtbl->Release(adapter);
|
||||
if (dxgiDev) dxgiDev->lpVtbl->Release(dxgiDev);
|
||||
|
||||
if (!st->out1 || FAILED(st->out1->lpVtbl->DuplicateOutput(st->out1,
|
||||
(IUnknown*)st->dev, &st->dup))) {
|
||||
fprintf(stderr, "eyes(dda): DuplicateOutput failed\n");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
DXGI_OUTDUPL_DESC dd;
|
||||
st->dup->lpVtbl->GetDesc(st->dup, &dd);
|
||||
st->W = dd.ModeDesc.Width;
|
||||
st->H = dd.ModeDesc.Height;
|
||||
|
||||
D3D11_TEXTURE2D_DESC td; memset(&td, 0, sizeof td);
|
||||
td.Width = st->W; td.Height = st->H; td.MipLevels = 1; td.ArraySize = 1;
|
||||
td.Format = DXGI_FORMAT_B8G8R8A8_UNORM; td.SampleDesc.Count = 1;
|
||||
td.Usage = D3D11_USAGE_STAGING; td.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||
if (FAILED(st->dev->lpVtbl->CreateTexture2D(st->dev, &td, NULL, &st->staging))) {
|
||||
fprintf(stderr, "eyes(dda): CreateTexture2D failed\n");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
capture_thread_arg* arg = (capture_thread_arg*)malloc(sizeof *arg);
|
||||
if (!arg) goto fail;
|
||||
arg->ctx = ctx; arg->fps = fps; arg->backend_state = st;
|
||||
|
||||
ctx->backend = VGPU_BK_DDA;
|
||||
ctx->draw_cursor_cap = 1; /* DDA frames are content-only → presenter draws cursor */
|
||||
|
||||
HANDLE t = CreateThread(NULL, 0, dda_thread, arg, 0, NULL);
|
||||
if (!t) { free(arg); goto fail; }
|
||||
CloseHandle(t);
|
||||
|
||||
fprintf(stderr, "eyes(dda): desktop %ux%u (content-only; cursor by presenter)\n",
|
||||
st->W, st->H);
|
||||
return 1;
|
||||
|
||||
fail:
|
||||
/* release any COM objects created before the failure (no ref leaks) */
|
||||
if (st->staging) st->staging->lpVtbl->Release(st->staging);
|
||||
if (st->dup) st->dup->lpVtbl->Release(st->dup);
|
||||
if (st->out1) st->out1->lpVtbl->Release(st->out1);
|
||||
if (st->dctx) st->dctx->lpVtbl->Release(st->dctx);
|
||||
if (st->dev) st->dev->lpVtbl->Release(st->dev);
|
||||
free(st);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef VGPU_CAPTURE_DDA_H
|
||||
#define VGPU_CAPTURE_DDA_H
|
||||
|
||||
/* capture_dda.h — DXGI Desktop Duplication capture backend (win32). */
|
||||
|
||||
#include "ctx.h" /* win32 vgpu_ctx */
|
||||
|
||||
int dda_start(vgpu_ctx* ctx, int fps);
|
||||
|
||||
#endif /* VGPU_CAPTURE_DDA_H */
|
||||
@@ -0,0 +1,79 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "capture_gdi.h"
|
||||
#include "capture-win32.h" /* capture_thread_arg (win32-private) */
|
||||
#include "present.h"
|
||||
#include "cursor.h" /* cursor_sample (position+shape+id) for compose+publish */
|
||||
#include "geometry.h" /* reactive geometry resample on capture-size change */
|
||||
#include "stream.h" /* vgpu_publish_cursor / vgpu_publish_cursor_shape */
|
||||
|
||||
static DWORD WINAPI gdi_thread(LPVOID param) {
|
||||
capture_thread_arg* arg = (capture_thread_arg*)param;
|
||||
vgpu_ctx* ctx = arg->ctx;
|
||||
int fps = arg->fps > 0 ? arg->fps : 30;
|
||||
free(arg);
|
||||
|
||||
HDC screen = GetDC(NULL);
|
||||
HDC mem = CreateCompatibleDC(screen);
|
||||
HBITMAP dib = NULL;
|
||||
void* bits = NULL;
|
||||
int W = 0, H = 0;
|
||||
const DWORD interval = (DWORD)(1000 / fps);
|
||||
|
||||
for (;;) {
|
||||
int w = GetSystemMetrics(SM_CXSCREEN), h = GetSystemMetrics(SM_CYSCREEN);
|
||||
if (w <= 0 || h <= 0) { Sleep(200); continue; }
|
||||
if (w != W || h != H || !dib) {
|
||||
if (dib) DeleteObject(dib);
|
||||
BITMAPINFO bi; memset(&bi, 0, sizeof bi);
|
||||
bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bi.bmiHeader.biWidth = w; bi.bmiHeader.biHeight = -h;
|
||||
bi.bmiHeader.biPlanes = 1; bi.bmiHeader.biBitCount = 32;
|
||||
bi.bmiHeader.biCompression = BI_RGB;
|
||||
dib = CreateDIBSection(screen, &bi, DIB_RGB_COLORS, &bits, NULL, 0);
|
||||
if (!dib) {
|
||||
fprintf(stderr, "eyes(gdi): CreateDIBSection %dx%d failed\n", w, h);
|
||||
Sleep(200); continue;
|
||||
}
|
||||
SelectObject(mem, dib);
|
||||
W = w; H = h;
|
||||
fprintf(stderr, "eyes(gdi): desktop %dx%d (BitBlt; cursor by presenter)\n", W, H);
|
||||
/* capture size changed (primary at origin (0,0)) → resample geometry */
|
||||
geometry_sample_and_publish(ctx, 0, 0);
|
||||
}
|
||||
if (BitBlt(mem, 0, 0, W, H, screen, 0, 0, SRCCOPY))
|
||||
vgpu_present_submit(ctx, (const uint8_t*)bits,
|
||||
(uint32_t)W, (uint32_t)H, (uint32_t)W * 4u);
|
||||
|
||||
/* source the cursor for present's compositing (under ctx->lock) and publish it */
|
||||
EnterCriticalSection(&ctx->lock);
|
||||
cursor_sample(ctx);
|
||||
uint32_t hx = (uint32_t)ctx->cursor.hot_x, hy = (uint32_t)ctx->cursor.hot_y;
|
||||
uint32_t gw = (uint32_t)ctx->cursor.gw, gh = (uint32_t)ctx->cursor.gh;
|
||||
uint32_t cid = (uint32_t)ctx->cursor.cursor_id;
|
||||
int32_t cx = (int32_t)ctx->cursor.x, cy = (int32_t)ctx->cursor.y;
|
||||
uint32_t cvis = (uint32_t)(ctx->cursor.visible != 0);
|
||||
LeaveCriticalSection(&ctx->lock);
|
||||
vgpu_publish_cursor_shape(&ctx->view, hx, hy, gw, gh, cid);
|
||||
vgpu_publish_cursor(&ctx->view, cx, cy, cvis);
|
||||
|
||||
Sleep(interval);
|
||||
}
|
||||
return 0; /* unreachable; satisfies -Wreturn-type */
|
||||
}
|
||||
|
||||
int gdi_start(vgpu_ctx* ctx, int fps) {
|
||||
ctx->backend = VGPU_BK_GDI;
|
||||
ctx->draw_cursor_cap = 1; /* GDI BitBlt excludes cursor → presenter draws it */
|
||||
|
||||
capture_thread_arg* arg = (capture_thread_arg*)malloc(sizeof *arg);
|
||||
if (!arg) return 0;
|
||||
arg->ctx = ctx; arg->fps = fps; arg->backend_state = NULL;
|
||||
HANDLE t = CreateThread(NULL, 0, gdi_thread, arg, 0, NULL);
|
||||
if (!t) { free(arg); return 0; }
|
||||
CloseHandle(t);
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef VGPU_CAPTURE_GDI_H
|
||||
#define VGPU_CAPTURE_GDI_H
|
||||
|
||||
/* capture_gdi.h — GDI BitBlt capture backend (win32, universal fallback). */
|
||||
|
||||
#include "ctx.h" /* win32 vgpu_ctx */
|
||||
|
||||
int gdi_start(vgpu_ctx* ctx, int fps);
|
||||
|
||||
#endif /* VGPU_CAPTURE_GDI_H */
|
||||
@@ -0,0 +1,162 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "capture_nvfbc.h"
|
||||
#include "capture-win32.h" /* capture_thread_arg (win32-private) */
|
||||
#include "present.h"
|
||||
#include "cursor.h" /* cursor_apply_shape / ctx->cursor */
|
||||
#include "geometry.h" /* reactive geometry resample on recreate */
|
||||
#include "stream.h" /* vgpu_publish_cursor / vgpu_publish_cursor_shape */
|
||||
#include "nvfbc_tosys_c.h"
|
||||
|
||||
typedef struct {
|
||||
NvFBCToSys_c* fbc;
|
||||
void* buf;
|
||||
NvFBC_CreateFunctionExType create;
|
||||
HCURSOR last_handle; /* shape-gate by HCURSOR change */
|
||||
} nvfbc_state;
|
||||
|
||||
/* Source the cursor for an NvFBC grab and publish it under the cursor_seq gate. NvFBC reports
|
||||
* only HW-cursor visibility (gi.bHWMouseVisible); position is not exposed, so one GetCursorInfo
|
||||
* per frame supplies x/y (the minimum possible). Shape is re-extracted only on HCURSOR change.
|
||||
* NvFBC composites the cursor itself (draw_cursor_cap==0) → present never reads ctx->cursor for
|
||||
* drawing, so no ctx->lock is required around the compose fields here.
|
||||
* gi.bProtectedContent / gi.dwSourcePID are available but out of scope (not in the contract). */
|
||||
static void nvfbc_source_cursor(vgpu_ctx* ctx, nvfbc_state* st,
|
||||
const NvFBCFrameGrabInfo* gi) {
|
||||
CURSORINFO ci; ci.cbSize = sizeof ci;
|
||||
int vis = gi->bHWMouseVisible ? 1 : 0;
|
||||
int x = ctx->cursor.x, y = ctx->cursor.y;
|
||||
if (GetCursorInfo(&ci)) {
|
||||
x = ci.ptScreenPos.x; y = ci.ptScreenPos.y;
|
||||
if (ci.hCursor && ci.hCursor != st->last_handle) {
|
||||
cursor_apply_shape(ctx, ci.hCursor);
|
||||
st->last_handle = ci.hCursor;
|
||||
}
|
||||
}
|
||||
ctx->cursor.visible = vis; ctx->cursor.x = x; ctx->cursor.y = y;
|
||||
|
||||
vgpu_publish_cursor_shape(&ctx->view,
|
||||
(uint32_t)ctx->cursor.hot_x, (uint32_t)ctx->cursor.hot_y,
|
||||
(uint32_t)ctx->cursor.gw, (uint32_t)ctx->cursor.gh,
|
||||
(uint32_t)ctx->cursor.cursor_id);
|
||||
vgpu_publish_cursor(&ctx->view, (int32_t)x, (int32_t)y, (uint32_t)vis);
|
||||
}
|
||||
|
||||
static NvFBCToSys_c* nvfbc_create(NvFBC_CreateFunctionExType pCreate, void** ppBuf) {
|
||||
NvFBCCreateParams cp; memset(&cp, 0, sizeof cp);
|
||||
cp.dwVersion = NVFBC_CREATE_PARAMS_VER;
|
||||
cp.dwInterfaceType = NVFBC_TO_SYS_C;
|
||||
cp.dwAdapterIdx = 0;
|
||||
if (pCreate(&cp) != NVFBC_SUCCESS || !cp.pNvFBC) return NULL;
|
||||
|
||||
NvFBCToSys_c* fbc = (NvFBCToSys_c*)cp.pNvFBC;
|
||||
*ppBuf = NULL;
|
||||
|
||||
NVFBC_TOSYS_SETUP_PARAMS_C sp; memset(&sp, 0, sizeof sp);
|
||||
sp.dwVersion = NVFBC_TOSYS_SETUP_PARAMS_VER_C;
|
||||
sp.bits = 1u; /* bWithHWCursor = 1 (bit 0) */
|
||||
sp.eMode = NVFBC_TOSYS_ARGB;
|
||||
sp.ppBuffer = ppBuf;
|
||||
if (fbc->lpVtbl->NvFBCToSysSetUp(fbc, &sp) != NVFBC_SUCCESS || !*ppBuf) {
|
||||
fbc->lpVtbl->NvFBCToSysRelease(fbc);
|
||||
return NULL;
|
||||
}
|
||||
return fbc;
|
||||
}
|
||||
|
||||
static DWORD WINAPI nvfbc_thread(LPVOID param) {
|
||||
capture_thread_arg* arg = (capture_thread_arg*)param;
|
||||
vgpu_ctx* ctx = arg->ctx;
|
||||
nvfbc_state* st = (nvfbc_state*)arg->backend_state;
|
||||
free(arg);
|
||||
|
||||
NvFBCToSys_c* fbc = st->fbc;
|
||||
void* buf = st->buf;
|
||||
|
||||
for (;;) {
|
||||
NvFBCFrameGrabInfo gi; memset(&gi, 0, sizeof gi);
|
||||
NVFBC_TOSYS_GRAB_FRAME_PARAMS_C gp; memset(&gp, 0, sizeof gp);
|
||||
gp.dwVersion = NVFBC_TOSYS_GRAB_FRAME_PARAMS_VER_C;
|
||||
gp.dwFlags = NVFBC_TOSYS_WAIT_WITH_TIMEOUT_C;
|
||||
gp.dwWaitTime = 1000;
|
||||
gp.eGMode = NVFBC_TOSYS_SOURCEMODE_FULL;
|
||||
gp.pNvFBCFrameGrabInfo = &gi;
|
||||
|
||||
NVFBCRESULT r = fbc->lpVtbl->NvFBCToSysGrabFrame(fbc, &gp);
|
||||
if (r != NVFBC_SUCCESS) {
|
||||
if (r == NVFBC_ERROR_INVALIDATED_SESSION || gi.bMustRecreate) {
|
||||
fprintf(stderr, "eyes(nvfbc): session invalidated (r=%d), recreating\n", (int)r);
|
||||
fbc->lpVtbl->NvFBCToSysRelease(fbc);
|
||||
fbc = NULL;
|
||||
while (!(fbc = nvfbc_create(st->create, &buf))) Sleep(200);
|
||||
st->fbc = fbc; st->buf = buf;
|
||||
/* grab session was recreated → display config may have changed: resample */
|
||||
geometry_sample_and_publish(ctx, 0, 0);
|
||||
} else {
|
||||
Sleep(50);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (gi.dwWidth && gi.dwHeight)
|
||||
vgpu_present_submit(ctx, (const uint8_t*)buf,
|
||||
gi.dwWidth, gi.dwHeight, gi.dwBufferWidth * 4u);
|
||||
nvfbc_source_cursor(ctx, st, &gi);
|
||||
}
|
||||
return 0; /* unreachable; satisfies -Wreturn-type */
|
||||
}
|
||||
|
||||
int nvfbc_start(vgpu_ctx* ctx, int fps) {
|
||||
(void)fps;
|
||||
HMODULE lib = LoadLibraryA("NvFBC64.dll");
|
||||
if (!lib) {
|
||||
fprintf(stderr, "eyes(nvfbc): LoadLibrary NvFBC64.dll failed (%lu)\n", GetLastError());
|
||||
return 0;
|
||||
}
|
||||
NvFBC_SetGlobalFlagsType pSetFlags = (NvFBC_SetGlobalFlagsType)(void*)GetProcAddress(lib, "NvFBC_SetGlobalFlags");
|
||||
NvFBC_EnableFunctionType pEnable = (NvFBC_EnableFunctionType)(void*)GetProcAddress(lib, "NvFBC_Enable");
|
||||
NvFBC_CreateFunctionExType pCreate = (NvFBC_CreateFunctionExType)(void*)GetProcAddress(lib, "NvFBC_CreateEx");
|
||||
NvFBC_GetStatusExFunctionType pStatus = (NvFBC_GetStatusExFunctionType)(void*)GetProcAddress(lib, "NvFBC_GetStatusEx");
|
||||
if (!pEnable || !pCreate || !pStatus) {
|
||||
fprintf(stderr, "eyes(nvfbc): missing exports\n");
|
||||
return 0;
|
||||
}
|
||||
if (pSetFlags) pSetFlags(NVFBC_GLOBAL_FLAGS_NO_INITIAL_REFRESH);
|
||||
if (pEnable(NVFBC_STATE_ENABLE) != NVFBC_SUCCESS) {
|
||||
fprintf(stderr, "eyes(nvfbc): NvFBC_Enable failed\n");
|
||||
return 0;
|
||||
}
|
||||
NvFBCStatusEx stx; memset(&stx, 0, sizeof stx);
|
||||
stx.dwVersion = NVFBC_STATUS_VER; stx.dwAdapterIdx = 0;
|
||||
if (pStatus(&stx) != NVFBC_SUCCESS || !stx.bIsCapturePossible) {
|
||||
fprintf(stderr, "eyes(nvfbc): capture NOT possible on this GPU/license\n");
|
||||
return 0;
|
||||
}
|
||||
void* buf = NULL;
|
||||
NvFBCToSys_c* fbc = nvfbc_create(pCreate, &buf);
|
||||
if (!fbc) {
|
||||
fprintf(stderr, "eyes(nvfbc): CreateEx/ToSysSetUp failed\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
nvfbc_state* st = (nvfbc_state*)malloc(sizeof *st);
|
||||
if (!st) { fbc->lpVtbl->NvFBCToSysRelease(fbc); return 0; }
|
||||
st->fbc = fbc; st->buf = buf; st->create = pCreate; st->last_handle = NULL;
|
||||
|
||||
capture_thread_arg* arg = (capture_thread_arg*)malloc(sizeof *arg);
|
||||
if (!arg) { fbc->lpVtbl->NvFBCToSysRelease(fbc); free(st); return 0; }
|
||||
arg->ctx = ctx; arg->fps = fps; arg->backend_state = st;
|
||||
|
||||
ctx->backend = VGPU_BK_NVFBC;
|
||||
ctx->draw_cursor_cap = 0; /* NvFBC composites HW cursor itself */
|
||||
|
||||
HANDLE t = CreateThread(NULL, 0, nvfbc_thread, arg, 0, NULL);
|
||||
if (!t) { fbc->lpVtbl->NvFBCToSysRelease(fbc); free(st); free(arg); return 0; }
|
||||
CloseHandle(t);
|
||||
|
||||
fprintf(stderr, "eyes(nvfbc): session up (ToSys ARGB/BGRA), iface=0x%lx\n",
|
||||
(unsigned long)stx.dwNvFBCVersion);
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef VGPU_CAPTURE_NVFBC_H
|
||||
#define VGPU_CAPTURE_NVFBC_H
|
||||
|
||||
/* capture_nvfbc.h — NVIDIA NvFBC ToSys capture backend (win32). */
|
||||
|
||||
#include "ctx.h" /* win32 vgpu_ctx */
|
||||
|
||||
int nvfbc_start(vgpu_ctx* ctx, int fps);
|
||||
|
||||
#endif /* VGPU_CAPTURE_NVFBC_H */
|
||||
@@ -0,0 +1,66 @@
|
||||
#ifndef VGPU_CTX_H
|
||||
#define VGPU_CTX_H
|
||||
|
||||
/* ctx.h — win32 runtime context. Embeds the neutral region-view (the engine's
|
||||
* borrowed handle onto the contract) alongside win32-owned staging/cursor/sync
|
||||
* state. Object = memory: ctx owns the staging arena and cursor state. */
|
||||
|
||||
#include <stdint.h>
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include "stream.h" /* vgpu_region_view (neutral contract handle) */
|
||||
#include "region.h" /* vgpu_region_t (win32 pinned region) */
|
||||
|
||||
/*
|
||||
* vgpu_ctx — the explicitly-passed context. Replaces all former g_* shared
|
||||
* state. Object = memory: ctx owns the producer staging arena and cursor
|
||||
* state; capture threads receive a vgpu_ctx* via their LPVOID thread param.
|
||||
*
|
||||
* Staging is a fixed arena sized for the max mode (no STL, no per-frame
|
||||
* malloc). content_buf holds the latest submitted desktop; frame_buf is the
|
||||
* composed (cursor-drawn) frame the publisher copies into a ring slot.
|
||||
*/
|
||||
|
||||
#define VGPU_STAGING_BYTES ((size_t)VGPU_MAX_WIDTH * VGPU_MAX_HEIGHT * 4u)
|
||||
|
||||
/* Cursor sample/compose state (GDI). Fixed buffers, no heap. */
|
||||
typedef struct {
|
||||
HCURSOR handle;
|
||||
int visible;
|
||||
int x, y;
|
||||
int hot_x, hot_y;
|
||||
int gw, gh; /* glyph dims */
|
||||
int cursor_id; /* VGPU_CURSOR_ID_* resolved on shape change */
|
||||
int mono; /* 1 = AND/XOR monochrome cursor */
|
||||
uint8_t* bgra; /* color cursor BGRA (arena) */
|
||||
uint8_t* and_mask; /* mono AND (arena) */
|
||||
uint8_t* xor_mask; /* mono XOR (arena) */
|
||||
} vgpu_cursor_t;
|
||||
|
||||
typedef struct vgpu_ctx {
|
||||
/* neutral contract handle (borrowed from region) — engine publishes through
|
||||
* this; win32 code reads region blocks via view.producer / view.control */
|
||||
vgpu_region_view view;
|
||||
|
||||
/* producer staging arena (owned) */
|
||||
uint8_t* arena; /* one VirtualAlloc block for all buffers */
|
||||
size_t arena_bytes;
|
||||
uint8_t* content_buf; /* latest submitted desktop, tight BGRA */
|
||||
uint8_t* frame_buf; /* composed frame to publish, tight BGRA */
|
||||
|
||||
/* submit handoff (capture thread -> publish pump) */
|
||||
CRITICAL_SECTION lock;
|
||||
HANDLE submit_event;
|
||||
int64_t content_seq; /* bumped on every submit */
|
||||
uint32_t content_w, content_h;
|
||||
|
||||
/* cursor */
|
||||
vgpu_cursor_t cursor;
|
||||
|
||||
/* runtime config (resolved from control) */
|
||||
uint32_t default_fps; /* fps from CLI; used when target_fps==0 */
|
||||
uint32_t backend; /* VGPU_BK_* chosen */
|
||||
int draw_cursor_cap; /* backend capability: does it need SW cursor */
|
||||
} vgpu_ctx;
|
||||
|
||||
#endif /* VGPU_CTX_H */
|
||||
@@ -0,0 +1,175 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <string.h>
|
||||
#include "cursor.h"
|
||||
#include "vgpu_stream.h" /* VGPU_CURSOR_ID_* */
|
||||
|
||||
/* Max supported cursor glyph; buffers are pre-arena'd in ctx (no heap here). */
|
||||
#define VGPU_CURSOR_MAX 256
|
||||
|
||||
static void read_mono(HBITMAP hbm, int w, int h, uint8_t* out /* w*h */) {
|
||||
int stride = ((w + 31) / 32) * 4;
|
||||
/* bounded scratch on stack: max (256/32*4)=32 bytes/row * 512 rows */
|
||||
static const int kMaxRows = VGPU_CURSOR_MAX * 2;
|
||||
uint8_t raw[(VGPU_CURSOR_MAX / 32 * 4) * (VGPU_CURSOR_MAX * 2)];
|
||||
if (h > kMaxRows) h = kMaxRows;
|
||||
if ((size_t)stride * h > sizeof raw) return;
|
||||
|
||||
struct { BITMAPINFOHEADER hdr; RGBQUAD pal[2]; } bi;
|
||||
memset(&bi, 0, sizeof bi);
|
||||
bi.hdr.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bi.hdr.biWidth = w; bi.hdr.biHeight = -h;
|
||||
bi.hdr.biPlanes = 1; bi.hdr.biBitCount = 1; bi.hdr.biCompression = BI_RGB;
|
||||
HDC dc = GetDC(NULL);
|
||||
GetDIBits(dc, hbm, 0, h, raw, (BITMAPINFO*)&bi, DIB_RGB_COLORS);
|
||||
ReleaseDC(NULL, dc);
|
||||
|
||||
memset(out, 0, (size_t)w * h);
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++) {
|
||||
int bit = 7 - (x & 7);
|
||||
out[(size_t)y * w + x] = (raw[(size_t)y * stride + (x >> 3)] >> bit) & 1u;
|
||||
}
|
||||
}
|
||||
|
||||
static void extract(vgpu_ctx* ctx, HCURSOR hc) {
|
||||
vgpu_cursor_t* cur = &ctx->cursor;
|
||||
cur->gw = cur->gh = 0;
|
||||
cur->mono = 0;
|
||||
|
||||
ICONINFO ii;
|
||||
if (!GetIconInfo(hc, &ii)) return;
|
||||
cur->hot_x = (int)ii.xHotspot;
|
||||
cur->hot_y = (int)ii.yHotspot;
|
||||
|
||||
if (ii.hbmColor) {
|
||||
BITMAP bm; GetObject(ii.hbmColor, sizeof bm, &bm);
|
||||
int w = bm.bmWidth, h = bm.bmHeight;
|
||||
if (w > VGPU_CURSOR_MAX) w = VGPU_CURSOR_MAX;
|
||||
if (h > VGPU_CURSOR_MAX) h = VGPU_CURSOR_MAX;
|
||||
BITMAPINFO bi; memset(&bi, 0, sizeof bi);
|
||||
bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bi.bmiHeader.biWidth = w; bi.bmiHeader.biHeight = -h;
|
||||
bi.bmiHeader.biPlanes = 1; bi.bmiHeader.biBitCount = 32;
|
||||
bi.bmiHeader.biCompression = BI_RGB;
|
||||
memset(cur->bgra, 0, (size_t)w * h * 4);
|
||||
HDC dc = GetDC(NULL);
|
||||
GetDIBits(dc, ii.hbmColor, 0, h, cur->bgra, &bi, DIB_RGB_COLORS);
|
||||
ReleaseDC(NULL, dc);
|
||||
cur->gw = w; cur->gh = h; cur->mono = 0;
|
||||
|
||||
int has_alpha = 0;
|
||||
for (size_t i = 0; i < (size_t)w * h; i++)
|
||||
if (cur->bgra[i * 4 + 3]) { has_alpha = 1; break; }
|
||||
if (!has_alpha && ii.hbmMask) {
|
||||
read_mono(ii.hbmMask, w, h, cur->and_mask);
|
||||
for (size_t i = 0; i < (size_t)w * h; i++)
|
||||
cur->bgra[i * 4 + 3] = cur->and_mask[i] ? 0 : 255;
|
||||
}
|
||||
} else if (ii.hbmMask) {
|
||||
BITMAP bm; GetObject(ii.hbmMask, sizeof bm, &bm);
|
||||
int w = bm.bmWidth, h = bm.bmHeight / 2;
|
||||
if (w > VGPU_CURSOR_MAX) w = VGPU_CURSOR_MAX;
|
||||
if (h > VGPU_CURSOR_MAX) h = VGPU_CURSOR_MAX;
|
||||
/* read both halves into a scratch laid over xor_mask region: reuse
|
||||
* and_mask for AND and xor_mask for XOR; read full into a stack pass */
|
||||
static uint8_t both[VGPU_CURSOR_MAX * VGPU_CURSOR_MAX * 2];
|
||||
read_mono(ii.hbmMask, w, bm.bmHeight, both);
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++) {
|
||||
cur->and_mask[(size_t)y * w + x] = both[(size_t)y * w + x];
|
||||
cur->xor_mask[(size_t)y * w + x] = both[(size_t)(y + h) * w + x];
|
||||
}
|
||||
cur->gw = w; cur->gh = h; cur->mono = 1;
|
||||
}
|
||||
if (ii.hbmColor) DeleteObject(ii.hbmColor);
|
||||
if (ii.hbmMask) DeleteObject(ii.hbmMask);
|
||||
}
|
||||
|
||||
int cursor_resolve_id(HCURSOR hc) {
|
||||
/* System-cursor table loaded once (IDC_* are stable per session). Lazy: built on first
|
||||
* call, then a linear handle compare. UNKNOWN for custom/unrecognized cursors. */
|
||||
static const struct { LPCTSTR idc; int id; } kSpec[] = {
|
||||
{ IDC_ARROW, VGPU_CURSOR_ID_ARROW },
|
||||
{ IDC_IBEAM, VGPU_CURSOR_ID_IBEAM },
|
||||
{ IDC_WAIT, VGPU_CURSOR_ID_WAIT },
|
||||
{ IDC_CROSS, VGPU_CURSOR_ID_CROSS },
|
||||
{ IDC_HAND, VGPU_CURSOR_ID_HAND },
|
||||
{ IDC_SIZENS, VGPU_CURSOR_ID_SIZENS },
|
||||
{ IDC_SIZEWE, VGPU_CURSOR_ID_SIZEWE },
|
||||
{ IDC_SIZENWSE, VGPU_CURSOR_ID_SIZENWSE },
|
||||
{ IDC_SIZENESW, VGPU_CURSOR_ID_SIZENESW },
|
||||
{ IDC_SIZEALL, VGPU_CURSOR_ID_SIZEALL },
|
||||
{ IDC_NO, VGPU_CURSOR_ID_NO },
|
||||
{ IDC_APPSTARTING, VGPU_CURSOR_ID_APPSTARTING },
|
||||
};
|
||||
enum { N = (int)(sizeof kSpec / sizeof kSpec[0]) };
|
||||
static HCURSOR cache[N];
|
||||
static int loaded = 0;
|
||||
if (!loaded) {
|
||||
for (int i = 0; i < N; i++) cache[i] = LoadCursor(NULL, kSpec[i].idc);
|
||||
loaded = 1;
|
||||
}
|
||||
if (!hc) return VGPU_CURSOR_ID_UNKNOWN;
|
||||
for (int i = 0; i < N; i++)
|
||||
if (cache[i] == hc) return kSpec[i].id;
|
||||
return VGPU_CURSOR_ID_UNKNOWN;
|
||||
}
|
||||
|
||||
void cursor_apply_shape(vgpu_ctx* ctx, HCURSOR hc) {
|
||||
extract(ctx, hc);
|
||||
ctx->cursor.cursor_id = cursor_resolve_id(hc);
|
||||
ctx->cursor.handle = hc;
|
||||
}
|
||||
|
||||
int cursor_sample(vgpu_ctx* ctx) {
|
||||
vgpu_cursor_t* cur = &ctx->cursor;
|
||||
CURSORINFO ci; ci.cbSize = sizeof ci;
|
||||
if (!GetCursorInfo(&ci)) {
|
||||
int changed = cur->visible;
|
||||
cur->visible = 0;
|
||||
return changed;
|
||||
}
|
||||
int vis = (ci.flags & CURSOR_SHOWING) != 0;
|
||||
int x = ci.ptScreenPos.x, y = ci.ptScreenPos.y;
|
||||
int changed = (vis != cur->visible) || (x != cur->x) || (y != cur->y)
|
||||
|| (ci.hCursor != cur->handle);
|
||||
if (vis && ci.hCursor && ci.hCursor != cur->handle) {
|
||||
extract(ctx, ci.hCursor);
|
||||
cur->cursor_id = cursor_resolve_id(ci.hCursor);
|
||||
cur->handle = ci.hCursor;
|
||||
}
|
||||
cur->visible = vis; cur->x = x; cur->y = y;
|
||||
return changed;
|
||||
}
|
||||
|
||||
void cursor_draw(vgpu_ctx* ctx, uint8_t* dst, uint32_t W, uint32_t H) {
|
||||
vgpu_cursor_t* cur = &ctx->cursor;
|
||||
if (!cur->visible || cur->gw == 0) return;
|
||||
int ox = cur->x - cur->hot_x, oy = cur->y - cur->hot_y;
|
||||
for (int gy = 0; gy < cur->gh; gy++) {
|
||||
int dy = oy + gy;
|
||||
if (dy < 0 || dy >= (int)H) continue;
|
||||
for (int gx = 0; gx < cur->gw; gx++) {
|
||||
int dx = ox + gx;
|
||||
if (dx < 0 || dx >= (int)W) continue;
|
||||
uint8_t* d = dst + ((size_t)dy * W + dx) * 4;
|
||||
if (!cur->mono) {
|
||||
const uint8_t* s = &cur->bgra[((size_t)gy * cur->gw + gx) * 4];
|
||||
uint32_t a = s[3];
|
||||
if (!a) continue;
|
||||
d[0] = (uint8_t)((s[0] * a + d[0] * (255 - a)) / 255);
|
||||
d[1] = (uint8_t)((s[1] * a + d[1] * (255 - a)) / 255);
|
||||
d[2] = (uint8_t)((s[2] * a + d[2] * (255 - a)) / 255);
|
||||
} else {
|
||||
int a = cur->and_mask[(size_t)gy * cur->gw + gx];
|
||||
int xr = cur->xor_mask[(size_t)gy * cur->gw + gx];
|
||||
if (a == 0 && xr == 0) { d[0] = d[1] = d[2] = 0; }
|
||||
else if (a == 0 && xr == 1) { d[0] = d[1] = d[2] = 255; }
|
||||
else if (a == 1 && xr == 1) { d[0] = (uint8_t)(255 - d[0]);
|
||||
d[1] = (uint8_t)(255 - d[1]);
|
||||
d[2] = (uint8_t)(255 - d[2]); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef VGPU_CURSOR_H
|
||||
#define VGPU_CURSOR_H
|
||||
|
||||
/* cursor.h — win32 GDI cursor sample/compose onto a tight BGRA frame. */
|
||||
|
||||
#include <stdint.h>
|
||||
#include "ctx.h" /* win32 vgpu_ctx (cursor state) */
|
||||
|
||||
/* Sample the current cursor (position/shape) into ctx->cursor.
|
||||
* Returns 1 if anything changed since last sample, else 0. */
|
||||
int cursor_sample(vgpu_ctx* ctx);
|
||||
|
||||
/* Resolve a HCURSOR to a VGPU_CURSOR_ID_* by comparing against the system cursor table
|
||||
* (LoadCursor(NULL, IDC_*) loaded once on first use). Returns VGPU_CURSOR_ID_UNKNOWN for
|
||||
* custom cursors. Not hot-path: called only under the shape-change gate. */
|
||||
int cursor_resolve_id(HCURSOR hc);
|
||||
|
||||
/* Extract glyph/hotspot/dims for hc into ctx->cursor, resolve its cursor_id, and record it as
|
||||
* the current handle. For backends that source position elsewhere (DDA from frame info) and
|
||||
* only need the shape on a shape-change gate. Caller serializes ctx->cursor writes. */
|
||||
void cursor_apply_shape(vgpu_ctx* ctx, HCURSOR hc);
|
||||
|
||||
/* Alpha/AND-XOR compose the sampled cursor onto a tight BGRA frame. */
|
||||
void cursor_draw(vgpu_ctx* ctx, uint8_t* bgra, uint32_t width, uint32_t height);
|
||||
|
||||
#endif /* VGPU_CURSOR_H */
|
||||
@@ -0,0 +1,52 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include "geometry.h"
|
||||
#include "stream.h" /* vgpu_publish_geometry */
|
||||
|
||||
/* GetDpiForMonitor lives in Shcore.dll (per-monitor DPI awareness API). Loaded dynamically so
|
||||
* the binary does not hard-depend on it; absence degrades dpi to "unknown" (0). */
|
||||
typedef HRESULT (WINAPI *GetDpiForMonitor_t)(HMONITOR, int /*MDT_*/, UINT*, UINT*);
|
||||
#define VGPU_MDT_EFFECTIVE_DPI 0
|
||||
|
||||
static UINT monitor_dpi(HMONITOR mon) {
|
||||
static GetDpiForMonitor_t fn = NULL;
|
||||
static int tried = 0;
|
||||
if (!tried) {
|
||||
HMODULE lib = LoadLibraryA("Shcore.dll");
|
||||
if (lib) fn = (GetDpiForMonitor_t)(void*)GetProcAddress(lib, "GetDpiForMonitor");
|
||||
tried = 1;
|
||||
}
|
||||
if (!fn || !mon) return 0u;
|
||||
UINT dx = 0, dy = 0;
|
||||
if (fn(mon, VGPU_MDT_EFFECTIVE_DPI, &dx, &dy) != S_OK || dx == 0u)
|
||||
return 0u;
|
||||
return dx;
|
||||
}
|
||||
|
||||
static uint32_t monitor_refresh_mhz(HMONITOR mon) {
|
||||
MONITORINFOEXW mi; mi.cbSize = sizeof mi;
|
||||
if (!mon || !GetMonitorInfoW(mon, (MONITORINFO*)&mi))
|
||||
return 0u;
|
||||
DEVMODEW dm; ZeroMemory(&dm, sizeof dm); dm.dmSize = sizeof dm;
|
||||
if (!EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm))
|
||||
return 0u;
|
||||
if (dm.dmDisplayFrequency <= 1u) /* 0/1 = hardware default, not a real rate */
|
||||
return 0u;
|
||||
return (uint32_t)dm.dmDisplayFrequency * 1000u; /* whole Hz -> milli-Hz */
|
||||
}
|
||||
|
||||
void geometry_sample_and_publish(vgpu_ctx* ctx, int32_t cap_x, int32_t cap_y) {
|
||||
int32_t virt_x = (int32_t)GetSystemMetrics(SM_XVIRTUALSCREEN);
|
||||
int32_t virt_y = (int32_t)GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
uint32_t virt_w = (uint32_t)GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
uint32_t virt_h = (uint32_t)GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
|
||||
POINT origin = { cap_x, cap_y };
|
||||
HMONITOR mon = MonitorFromPoint(origin, MONITOR_DEFAULTTOPRIMARY);
|
||||
|
||||
uint32_t dpi = monitor_dpi(mon);
|
||||
uint32_t refresh = monitor_refresh_mhz(mon);
|
||||
|
||||
vgpu_publish_geometry(&ctx->view, virt_x, virt_y, virt_w, virt_h,
|
||||
cap_x, cap_y, dpi, refresh);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#ifndef VGPU_GEOMETRY_H
|
||||
#define VGPU_GEOMETRY_H
|
||||
|
||||
/* geometry.h — win32 display-geometry sampler. Samples the virtual-desktop bbox plus the
|
||||
* captured output's origin / DPI / refresh and publishes them under the geom_seq seqlock.
|
||||
* Not per-frame: called once at session start and reactively on backend recreate / capture-
|
||||
* size change (the captured surface SIZE itself travels in desc.width/height, not here). */
|
||||
|
||||
#include <stdint.h>
|
||||
#include "ctx.h" /* win32 vgpu_ctx (region-view) */
|
||||
|
||||
/* Sample display geometry for the captured output whose top-left origin is (cap_x,cap_y) in
|
||||
* virtual-desktop coordinates, and publish it. cap_x/cap_y is (0,0) for primary/full-screen
|
||||
* backends and the duplicated output's DesktopCoordinates for DDA. The captured size is taken
|
||||
* from desc.width/height and is not sampled here. */
|
||||
void geometry_sample_and_publish(vgpu_ctx* ctx, int32_t cap_x, int32_t cap_y);
|
||||
|
||||
#endif /* VGPU_GEOMETRY_H */
|
||||
@@ -0,0 +1,55 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "region.h" /* win32 pinned region */
|
||||
#include "ctx.h" /* win32 vgpu_ctx (embeds region-view) */
|
||||
#include "present.h" /* present/pump lifecycle */
|
||||
#include "stream.h" /* OS-agnostic status/error/backend setters */
|
||||
#include "capture.h" /* backend table */
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
int fps = argc > 1 ? atoi(argv[1]) : 30;
|
||||
if (fps <= 0) fps = 30;
|
||||
|
||||
vgpu_region_t region;
|
||||
if (vgpu_region_create(®ion) != 0) {
|
||||
fprintf(stderr, "main: region_create failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
vgpu_ctx ctx;
|
||||
if (vgpu_present_init(&ctx, ®ion, (uint32_t)fps) != 0) {
|
||||
fprintf(stderr, "main: present_init failed\n");
|
||||
vgpu_region_destroy(®ion);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* eyes = getenv("EYES");
|
||||
int n = 0;
|
||||
const capture_backend* bks = capture_backends(&n);
|
||||
int started = 0;
|
||||
for (int i = 0; i < n && !started; i++) {
|
||||
if (eyes && _stricmp(eyes, bks[i].name) != 0) continue;
|
||||
fprintf(stderr, "eyes: trying %s\n", bks[i].name);
|
||||
started = bks[i].start(&ctx, fps);
|
||||
if (!started) fprintf(stderr, "eyes: %s unavailable\n", bks[i].name);
|
||||
}
|
||||
if (!started) {
|
||||
fprintf(stderr, "eyes: no capture backend available\n");
|
||||
vgpu_set_status(&ctx.view, VGPU_ST_ERROR);
|
||||
vgpu_set_error(&ctx.view, 2u);
|
||||
vgpu_present_deinit(&ctx);
|
||||
vgpu_region_destroy(®ion);
|
||||
return 1;
|
||||
}
|
||||
|
||||
vgpu_set_backend(&ctx.view, ctx.backend);
|
||||
vgpu_present_run(&ctx); /* never returns */
|
||||
|
||||
vgpu_present_deinit(&ctx);
|
||||
vgpu_region_destroy(®ion);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
#ifndef VGPU_NVFBC_TOSYS_C_H
|
||||
#define VGPU_NVFBC_TOSYS_C_H
|
||||
|
||||
/*
|
||||
* C mirror of NvFBC's ToSys interface. The vendor header
|
||||
* third_party/NvFBC/nvFBCToSys.h declares INvFBCToSys_v3 as a C++ abstract
|
||||
* class (vtable of 5 pure-virtual
|
||||
* __stdcall methods). We do NOT edit the vendor header; instead we replicate its
|
||||
* single-inheritance vtable ABI as a COM-in-C interface so the producer stays
|
||||
* pure C. Slot order MUST match declaration order in nvFBCToSys.h:
|
||||
* 0 NvFBCToSysSetUp
|
||||
* 1 NvFBCToSysGrabFrame
|
||||
* 2 NvFBCToSysCursorCapture
|
||||
* 3 NvFBCToSysGPUBasedCPUSleep
|
||||
* 4 NvFBCToSysRelease
|
||||
* On x64 (mingw/MSVC) `this` is the implicit first integer argument; __stdcall
|
||||
* is a no-op for x64 so a plain pointer arg matches the vtable slot.
|
||||
*/
|
||||
|
||||
#include "NvFBC/nvFBC.h" /* vendor (third_party/): NVFBCRESULT, NvU32, param structs */
|
||||
|
||||
/* SetUp / GrabFrame param structs come from nvFBCToSys.h, but that header is C++.
|
||||
* Redeclare the two we use here (layout-identical, C-clean). */
|
||||
|
||||
typedef enum {
|
||||
NVFBC_TOSYS_ARGB = 0,
|
||||
NVFBC_TOSYS_RGB,
|
||||
NVFBC_TOSYS_YYYYUV420p,
|
||||
NVFBC_TOSYS_RGB_PLANAR,
|
||||
NVFBC_TOSYS_XOR,
|
||||
NVFBC_TOSYS_YUV444p,
|
||||
NVFBC_TOSYS_BUF_FMT_LAST
|
||||
} NVFBCToSysBufferFormat_c;
|
||||
|
||||
typedef enum {
|
||||
NVFBC_TOSYS_SOURCEMODE_FULL = 0,
|
||||
NVFBC_TOSYS_SOURCEMODE_SCALE,
|
||||
NVFBC_TOSYS_SOURCEMODE_CROP,
|
||||
NVFBC_TOSYS_SOURCEMODE_LAST
|
||||
} NVFBCToSysGrabMode_c;
|
||||
|
||||
enum {
|
||||
NVFBC_TOSYS_NOFLAGS_C = 0x0,
|
||||
NVFBC_TOSYS_NOWAIT_C = 0x1,
|
||||
NVFBC_TOSYS_WAIT_WITH_TIMEOUT_C = 0x10
|
||||
};
|
||||
|
||||
#define NVFBC_TO_SYS_C (0x1204)
|
||||
|
||||
typedef struct {
|
||||
NvU32 dwVersion;
|
||||
NvU32 bits; /* bWithHWCursor:1, bDiffMap:1, bSep:1, rsvd:29 */
|
||||
NVFBCToSysBufferFormat_c eMode;
|
||||
NvU32 dwReserved1;
|
||||
void **ppBuffer;
|
||||
void **ppDiffMap;
|
||||
void *hCursorCaptureEvent;
|
||||
NvU32 dwReserved[58];
|
||||
void *pReserved[29];
|
||||
} NVFBC_TOSYS_SETUP_PARAMS_C;
|
||||
#define NVFBC_TOSYS_SETUP_PARAMS_VER_C \
|
||||
NVFBC_STRUCT_VERSION(NVFBC_TOSYS_SETUP_PARAMS_C, 2)
|
||||
|
||||
typedef struct {
|
||||
NvU32 dwVersion;
|
||||
NvU32 dwFlags;
|
||||
NvU32 dwTargetWidth;
|
||||
NvU32 dwTargetHeight;
|
||||
NvU32 dwStartX;
|
||||
NvU32 dwStartY;
|
||||
NVFBCToSysGrabMode_c eGMode;
|
||||
NvU32 dwWaitTime;
|
||||
NvFBCFrameGrabInfo *pNvFBCFrameGrabInfo;
|
||||
NvU32 dwReserved[56];
|
||||
void *pReserved[31];
|
||||
} NVFBC_TOSYS_GRAB_FRAME_PARAMS_C;
|
||||
#define NVFBC_TOSYS_GRAB_FRAME_PARAMS_VER_C \
|
||||
NVFBC_STRUCT_VERSION(NVFBC_TOSYS_GRAB_FRAME_PARAMS_C, 1)
|
||||
|
||||
/* COM-in-C interface mirror */
|
||||
typedef struct NvFBCToSys_c NvFBCToSys_c;
|
||||
typedef struct {
|
||||
NVFBCRESULT (__stdcall *NvFBCToSysSetUp)(NvFBCToSys_c*, NVFBC_TOSYS_SETUP_PARAMS_C*);
|
||||
NVFBCRESULT (__stdcall *NvFBCToSysGrabFrame)(NvFBCToSys_c*, NVFBC_TOSYS_GRAB_FRAME_PARAMS_C*);
|
||||
NVFBCRESULT (__stdcall *NvFBCToSysCursorCapture)(NvFBCToSys_c*, void*);
|
||||
NVFBCRESULT (__stdcall *NvFBCToSysGPUBasedCPUSleep)(NvFBCToSys_c*, __int64);
|
||||
NVFBCRESULT (__stdcall *NvFBCToSysRelease)(NvFBCToSys_c*);
|
||||
} NvFBCToSys_c_vtbl;
|
||||
struct NvFBCToSys_c {
|
||||
const NvFBCToSys_c_vtbl* lpVtbl;
|
||||
};
|
||||
|
||||
#endif /* VGPU_NVFBC_TOSYS_C_H */
|
||||
@@ -0,0 +1,212 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include "present.h"
|
||||
#include "stream.h" /* OS-agnostic publish / control API + region-view */
|
||||
#include "cursor.h"
|
||||
#include "geometry.h" /* one-shot display-geometry sample at session start */
|
||||
|
||||
/* cursor arena sizing */
|
||||
#define VGPU_CUR_MAX 256u
|
||||
#define VGPU_CUR_BGRA (VGPU_CUR_MAX * VGPU_CUR_MAX * 4u)
|
||||
#define VGPU_CUR_MASK (VGPU_CUR_MAX * VGPU_CUR_MAX)
|
||||
|
||||
static uint64_t now_ns(void) {
|
||||
static LARGE_INTEGER freq = { .QuadPart = 0 };
|
||||
if (freq.QuadPart == 0) QueryPerformanceFrequency(&freq);
|
||||
LARGE_INTEGER c; QueryPerformanceCounter(&c);
|
||||
return (uint64_t)((double)c.QuadPart * 1e9 / (double)freq.QuadPart);
|
||||
}
|
||||
|
||||
int vgpu_present_init(vgpu_ctx* ctx, vgpu_region_t* region, uint32_t default_fps) {
|
||||
memset(ctx, 0, sizeof *ctx);
|
||||
ctx->view.producer = region->producer;
|
||||
ctx->view.control = region->control;
|
||||
ctx->view.ring = region->ring;
|
||||
ctx->default_fps = default_fps ? default_fps : 30u;
|
||||
ctx->backend = VGPU_BK_NONE;
|
||||
ctx->draw_cursor_cap = 1;
|
||||
|
||||
/* one arena: content + frame + cursor buffers */
|
||||
size_t bytes = VGPU_STAGING_BYTES /* content */
|
||||
+ VGPU_STAGING_BYTES /* frame */
|
||||
+ VGPU_CUR_BGRA /* cursor bgra */
|
||||
+ VGPU_CUR_MASK /* and */
|
||||
+ VGPU_CUR_MASK; /* xor */
|
||||
uint8_t* a = (uint8_t*)VirtualAlloc(NULL, bytes, MEM_RESERVE | MEM_COMMIT,
|
||||
PAGE_READWRITE);
|
||||
if (!a) {
|
||||
fprintf(stderr, "present: arena VirtualAlloc %zu MiB failed (%lu)\n",
|
||||
bytes / (1024 * 1024), GetLastError());
|
||||
return 1;
|
||||
}
|
||||
ctx->arena = a;
|
||||
ctx->arena_bytes = bytes;
|
||||
|
||||
size_t off = 0;
|
||||
ctx->content_buf = a + off; off += VGPU_STAGING_BYTES;
|
||||
ctx->frame_buf = a + off; off += VGPU_STAGING_BYTES;
|
||||
ctx->cursor.bgra = a + off; off += VGPU_CUR_BGRA;
|
||||
ctx->cursor.and_mask = a + off; off += VGPU_CUR_MASK;
|
||||
ctx->cursor.xor_mask = a + off; off += VGPU_CUR_MASK;
|
||||
|
||||
InitializeCriticalSection(&ctx->lock);
|
||||
ctx->submit_event = CreateEvent(NULL, FALSE, FALSE, NULL);
|
||||
ctx->content_seq = 0;
|
||||
ctx->content_w = ctx->content_h = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void vgpu_present_deinit(vgpu_ctx* ctx) {
|
||||
if (ctx->submit_event) { CloseHandle(ctx->submit_event); ctx->submit_event = NULL; }
|
||||
DeleteCriticalSection(&ctx->lock);
|
||||
if (ctx->arena) { VirtualFree(ctx->arena, 0, MEM_RELEASE); ctx->arena = NULL; }
|
||||
}
|
||||
|
||||
void vgpu_present_submit(vgpu_ctx* ctx, const uint8_t* src,
|
||||
uint32_t W, uint32_t H, uint32_t src_pitch) {
|
||||
if (W > VGPU_MAX_WIDTH) W = VGPU_MAX_WIDTH;
|
||||
if (H > VGPU_MAX_HEIGHT) H = VGPU_MAX_HEIGHT;
|
||||
if (W == 0 || H == 0) return;
|
||||
|
||||
EnterCriticalSection(&ctx->lock);
|
||||
uint8_t* d = ctx->content_buf;
|
||||
const uint32_t row = W * 4u;
|
||||
for (uint32_t y = 0; y < H; y++)
|
||||
memcpy(d + (size_t)y * row, src + (size_t)y * src_pitch, row);
|
||||
ctx->content_w = W;
|
||||
ctx->content_h = H;
|
||||
ctx->content_seq++;
|
||||
LeaveCriticalSection(&ctx->lock);
|
||||
/* static-idle: stamp the moment the source delivered new content (the raw perception;
|
||||
* the host derives "ms idle" from its own clock). Single 8-aligned MOV, off the lock. */
|
||||
vgpu_publish_content_change(&ctx->view, now_ns());
|
||||
SetEvent(ctx->submit_event);
|
||||
}
|
||||
|
||||
void vgpu_present_run(vgpu_ctx* ctx) {
|
||||
const vgpu_region_view* rv = &ctx->view; /* neutral handle for the engine */
|
||||
const DWORD poll_ms = 8;
|
||||
int64_t last_seq = -1;
|
||||
uint32_t prev_state = VGPU_CMD_STOP;
|
||||
uint32_t last_ff_ack = rv->producer->full_frame_ack;
|
||||
DWORD last_beat = GetTickCount();
|
||||
uint64_t last_publish_ns = 0; /* 0 → first eligible frame publishes immediately */
|
||||
int last_cur_x = 0, last_cur_y = 0, last_cur_vis = 0;
|
||||
HCURSOR last_cur_handle = NULL;
|
||||
|
||||
/* one-shot display geometry: publish once before the loop (flat pull contract). The
|
||||
* captured-output origin is (0,0) for the primary/full-screen capture path; backends
|
||||
* resample reactively on recreate / capture-size change. No periodic poll in the loop. */
|
||||
geometry_sample_and_publish(ctx, 0, 0);
|
||||
|
||||
for (;;) {
|
||||
WaitForSingleObject(ctx->submit_event, poll_ms);
|
||||
|
||||
/* --- heartbeat: always ticks, independent of desired_state --- */
|
||||
DWORD nowt = GetTickCount();
|
||||
if (nowt - last_beat >= VGPU_HEARTBEAT_PERIOD_MS) {
|
||||
vgpu_tick_heartbeat(rv);
|
||||
last_beat = nowt;
|
||||
}
|
||||
|
||||
/* --- reconcile control (gen-seqlock -> apply -> ack) --- */
|
||||
vgpu_control_view cv;
|
||||
uint32_t desired = prev_state;
|
||||
uint32_t draw_cursor = 1;
|
||||
int force_full = 0;
|
||||
uint32_t fps = ctx->default_fps; /* publish-rate cap (applied) */
|
||||
uint32_t ff_req = last_ff_ack; /* full_frame_req value to honor */
|
||||
if (vgpu_control_read(rv, &cv)) {
|
||||
desired = cv.desired_state;
|
||||
draw_cursor = cv.draw_cursor;
|
||||
fps = cv.target_fps ? cv.target_fps : ctx->default_fps;
|
||||
vgpu_set_applied_fps(rv, fps);
|
||||
vgpu_publish_ctrl_ack(rv, cv.gen);
|
||||
|
||||
ff_req = cv.full_frame_req;
|
||||
if ((ff_req - last_ff_ack) != 0u)
|
||||
force_full = 1; /* edge pending, wrap-tolerant */
|
||||
}
|
||||
|
||||
/* --- lifecycle transitions --- */
|
||||
if (desired != prev_state) {
|
||||
if (desired == VGPU_CMD_RUN && prev_state != VGPU_CMD_RUN) {
|
||||
vgpu_bump_run_epoch(rv);
|
||||
vgpu_set_status(rv, VGPU_ST_CAPTURING);
|
||||
force_full = 1; /* fresh frame on start */
|
||||
} else if (desired == VGPU_CMD_PAUSE) {
|
||||
vgpu_set_status(rv, VGPU_ST_PAUSED);
|
||||
} else if (desired == VGPU_CMD_STOP) {
|
||||
vgpu_set_status(rv, VGPU_ST_STOPPED);
|
||||
}
|
||||
prev_state = desired;
|
||||
} else if (last_seq < 0 && desired == VGPU_CMD_RUN) {
|
||||
vgpu_set_status(rv, VGPU_ST_CAPTURING);
|
||||
}
|
||||
|
||||
if (desired != VGPU_CMD_RUN) {
|
||||
/* PAUSED/STOPPED: no new frames; heartbeat still ticks. We do NOT
|
||||
* ack a pending full_frame here — acking without publishing would
|
||||
* be a false "honored". A pending request is honored on the next
|
||||
* transition to RUN (force_full=1 there → publish + ack). */
|
||||
continue;
|
||||
}
|
||||
|
||||
/* --- compose + publish on content change OR forced full frame, but
|
||||
* rate-limited to the applied fps cap (the single publish point →
|
||||
* contract-level cap, independent of the capture backend). A
|
||||
* force_full bypasses the cap (due=1). present does NOT sample the
|
||||
* cursor (capture threads source it); it only reads ctx->cursor under
|
||||
* ctx->lock for compositing, and detects cursor motion via a delta so
|
||||
* a pure cursor move over static desktop still recomposes. --- */
|
||||
uint64_t interval_ns = fps > 0 ? (1000000000ull / fps) : 0;
|
||||
uint64_t now = now_ns();
|
||||
int due = force_full || interval_ns == 0
|
||||
|| (now - last_publish_ns) >= interval_ns;
|
||||
|
||||
int compose_cursor = (ctx->draw_cursor_cap && draw_cursor);
|
||||
|
||||
EnterCriticalSection(&ctx->lock);
|
||||
int64_t seq = ctx->content_seq;
|
||||
uint32_t W = ctx->content_w, H = ctx->content_h;
|
||||
int cur_changed = compose_cursor
|
||||
&& ((ctx->cursor.visible != last_cur_vis)
|
||||
|| (ctx->cursor.x != last_cur_x)
|
||||
|| (ctx->cursor.y != last_cur_y)
|
||||
|| (ctx->cursor.handle != last_cur_handle));
|
||||
int have = (W && H);
|
||||
int content_new = have && (seq != last_seq || cur_changed || force_full);
|
||||
/* take the frame ONLY when due — so we never drop the latest content;
|
||||
* if not due, last_seq is left untouched and it publishes next due. */
|
||||
int dirty = content_new && due;
|
||||
if (dirty) {
|
||||
memcpy(ctx->frame_buf, ctx->content_buf, (size_t)W * H * 4u);
|
||||
last_seq = seq;
|
||||
if (compose_cursor)
|
||||
cursor_draw(ctx, ctx->frame_buf, W, H);
|
||||
last_cur_vis = ctx->cursor.visible;
|
||||
last_cur_x = ctx->cursor.x; last_cur_y = ctx->cursor.y;
|
||||
last_cur_handle = ctx->cursor.handle;
|
||||
}
|
||||
LeaveCriticalSection(&ctx->lock);
|
||||
|
||||
if (!dirty) {
|
||||
/* not due, or nothing to publish. A force_full with content has
|
||||
* due=1 → dirty=1, so it never lands here while have is true; thus
|
||||
* no spurious ack edge. */
|
||||
continue;
|
||||
}
|
||||
|
||||
if (vgpu_publish_frame(rv, ctx->frame_buf, W, H, now) == 0) {
|
||||
last_publish_ns = now;
|
||||
if (force_full) {
|
||||
vgpu_publish_full_frame_ack(rv, ff_req);
|
||||
last_ff_ack = ff_req;
|
||||
}
|
||||
} else {
|
||||
vgpu_set_error(rv, 1u); /* frame too large for slot (mode > max) */
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef VGPU_PRESENT_H
|
||||
#define VGPU_PRESENT_H
|
||||
|
||||
/* present.h — win32 present/pump lifecycle: staging arena, submit handoff, and
|
||||
* the publish loop driving the OS-agnostic engine over ctx's region-view. */
|
||||
|
||||
#include <stdint.h>
|
||||
#include "ctx.h" /* win32 vgpu_ctx + vgpu_region_t */
|
||||
|
||||
/* Initialize present/staging state inside ctx over an already-created region.
|
||||
* Allocates the staging+cursor arena. Returns 0 on success. */
|
||||
int vgpu_present_init(vgpu_ctx* ctx, vgpu_region_t* region, uint32_t default_fps);
|
||||
void vgpu_present_deinit(vgpu_ctx* ctx);
|
||||
|
||||
/* Capture backends submit a freshly captured desktop frame (any source pitch).
|
||||
* Repacked tight into ctx->content_buf, clamped to max mode. Thread-safe. */
|
||||
void vgpu_present_submit(vgpu_ctx* ctx, const uint8_t* bgra,
|
||||
uint32_t width, uint32_t height, uint32_t src_pitch);
|
||||
|
||||
/* Run the publish pump: reconcile control, tick heartbeat, compose cursor,
|
||||
* publish on change / on full_frame_req. Never returns (process lifetime). */
|
||||
void vgpu_present_run(vgpu_ctx* ctx);
|
||||
|
||||
#endif /* VGPU_PRESENT_H */
|
||||
@@ -0,0 +1,172 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "region.h"
|
||||
#include "atomic-shim.h" /* x86-TSO ordering for contract init publish */
|
||||
|
||||
#define VGPU_2MB (2u * 1024u * 1024u)
|
||||
|
||||
/* Page-segregated init of the contract over an already-pinned region base.
|
||||
* Init-ordering per contract: status=INIT, latest=NONE, backend, supported_formats,
|
||||
* release-barrier; heartbeat starts later (in the run pump). */
|
||||
static void region_init_contract(vgpu_region_t* r) {
|
||||
vgpu_producer_t* p = r->producer;
|
||||
vgpu_control_t* c = r->control;
|
||||
|
||||
memset(p, 0, sizeof *p);
|
||||
memset(c, 0, sizeof *c);
|
||||
|
||||
p->status = VGPU_ST_INIT;
|
||||
p->backend = VGPU_BK_NONE;
|
||||
p->error_code = 0;
|
||||
p->applied_fps = 0;
|
||||
p->supported_formats = (1u << VGPU_FMT_BGRA8888);
|
||||
p->run_epoch = 0;
|
||||
p->heartbeat = 0;
|
||||
p->frame_id = 0;
|
||||
p->ctrl_ack = 0;
|
||||
p->full_frame_ack = 0;
|
||||
for (uint32_t i = 0; i < VGPU_SLOT_COUNT; i++)
|
||||
p->seq[i] = 0;
|
||||
|
||||
/* control starts RUN: producer captures immediately; host may STOP/PAUSE */
|
||||
c->ctrl_gen = 0;
|
||||
c->desired_state = VGPU_CMD_RUN;
|
||||
c->target_fps = 0;
|
||||
c->draw_cursor = 1;
|
||||
c->full_frame_req = 0;
|
||||
c->consumer_tick = 0;
|
||||
c->attached = 0;
|
||||
|
||||
/* publish latest last with a release store gating all of the above */
|
||||
vgpu_sfence();
|
||||
vgpu_store_release32(&p->latest, VGPU_LATEST_NONE);
|
||||
}
|
||||
|
||||
static int adjust_lock_memory_privilege(void) {
|
||||
HANDLE tok;
|
||||
if (!OpenProcessToken(GetCurrentProcess(),
|
||||
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &tok))
|
||||
return 0;
|
||||
TOKEN_PRIVILEGES tp;
|
||||
memset(&tp, 0, sizeof tp);
|
||||
tp.PrivilegeCount = 1;
|
||||
if (!LookupPrivilegeValueA(NULL, SE_LOCK_MEMORY_NAME, &tp.Privileges[0].Luid)) {
|
||||
CloseHandle(tok);
|
||||
return 0;
|
||||
}
|
||||
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
|
||||
int ok = AdjustTokenPrivileges(tok, FALSE, &tp, sizeof tp, NULL, NULL)
|
||||
&& GetLastError() == ERROR_SUCCESS;
|
||||
CloseHandle(tok);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int vgpu_region_create(vgpu_region_t* out) {
|
||||
memset(out, 0, sizeof *out);
|
||||
|
||||
const uint64_t bytes = VGPU_REGION_BYTES;
|
||||
|
||||
void* os_base = NULL;
|
||||
uint8_t* base = NULL;
|
||||
uint64_t os_total = 0;
|
||||
|
||||
if (adjust_lock_memory_privilege()) {
|
||||
SIZE_T large_min = GetLargePageMinimum();
|
||||
if (large_min && large_min <= VGPU_2MB) {
|
||||
SIZE_T rounded = (SIZE_T)((bytes + VGPU_2MB - 1) & ~(uint64_t)(VGPU_2MB - 1));
|
||||
void* p = VirtualAlloc(NULL, rounded,
|
||||
MEM_RESERVE | MEM_COMMIT | MEM_LARGE_PAGES,
|
||||
PAGE_READWRITE);
|
||||
if (p) {
|
||||
/* large pages are >= 2 MiB → base is already 2 MiB-aligned */
|
||||
os_base = p;
|
||||
base = (uint8_t*)p;
|
||||
os_total = rounded;
|
||||
fprintf(stderr, "region: MEM_LARGE_PAGES %llu MiB at %p\n",
|
||||
(unsigned long long)(rounded / (1024 * 1024)), p);
|
||||
} else {
|
||||
fprintf(stderr, "region: MEM_LARGE_PAGES failed (%lu), fallback\n",
|
||||
GetLastError());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "region: SE_LOCK_MEMORY unavailable, fallback\n");
|
||||
}
|
||||
|
||||
if (!base) {
|
||||
uint64_t total = bytes + VGPU_2MB;
|
||||
void* p = VirtualAlloc(NULL, (SIZE_T)total, MEM_RESERVE | MEM_COMMIT,
|
||||
PAGE_READWRITE);
|
||||
if (!p) {
|
||||
fprintf(stderr, "region: VirtualAlloc %llu MiB failed (%lu)\n",
|
||||
(unsigned long long)(total / (1024 * 1024)), GetLastError());
|
||||
return 1;
|
||||
}
|
||||
uintptr_t addr = (uintptr_t)p;
|
||||
uintptr_t aligned = (addr + VGPU_2MB - 1) & ~(uintptr_t)(VGPU_2MB - 1);
|
||||
|
||||
/* The region must be RESIDENT, not merely committed: the host reads it out
|
||||
* of guest RAM and only PRESENT pages are visible to it — a committed but
|
||||
* demand-zero page has no PTE, so it is unreadable from the host. VirtualLock
|
||||
* pins the pages into the working set, but it can lock at most the process
|
||||
* MINIMUM working set, and the default quota is far below the region size
|
||||
* (so a bare VirtualLock fails with ERROR_WORKING_SET_QUOTA). Raise the
|
||||
* minimum first. NB: VirtualLock / SetProcessWorkingSetSize do NOT need
|
||||
* SE_LOCK_MEMORY — that privilege is only for large pages / AWE. */
|
||||
SIZE_T ws_min = (SIZE_T)(bytes + 64ull * 1024 * 1024); /* region + headroom */
|
||||
SIZE_T ws_max = ws_min + 128ull * 1024 * 1024;
|
||||
SIZE_T cur_min = 0, cur_max = 0;
|
||||
if (GetProcessWorkingSetSize(GetCurrentProcess(), &cur_min, &cur_max)) {
|
||||
if (cur_min > ws_min) ws_min = cur_min; /* never shrink an existing quota */
|
||||
if (cur_max > ws_max) ws_max = cur_max;
|
||||
}
|
||||
if (!SetProcessWorkingSetSize(GetCurrentProcess(), ws_min, ws_max))
|
||||
fprintf(stderr, "region: SetProcessWorkingSetSize(%llu MiB) failed (%lu)\n",
|
||||
(unsigned long long)(ws_min / (1024 * 1024)), GetLastError());
|
||||
|
||||
if (!VirtualLock((void*)aligned, (SIZE_T)bytes)) {
|
||||
fprintf(stderr, "region: VirtualLock failed (%lu) — pre-faulting region\n",
|
||||
GetLastError());
|
||||
/* Last resort: fault every page so it is at least PRESENT now. Without
|
||||
* the lock the trimmer may evict it under pressure, but the raised
|
||||
* minimum working set above makes eviction far less likely. */
|
||||
volatile uint8_t* q = (volatile uint8_t*)aligned;
|
||||
for (uint64_t off = 0; off < bytes; off += 4096u) q[off] = q[off];
|
||||
}
|
||||
|
||||
os_base = p;
|
||||
base = (uint8_t*)aligned;
|
||||
os_total = total;
|
||||
fprintf(stderr, "region: fallback VirtualAlloc+lock %llu MiB, aligned at %p\n",
|
||||
(unsigned long long)(bytes / (1024 * 1024)), (void*)aligned);
|
||||
}
|
||||
|
||||
if (((uintptr_t)base & (VGPU_2MB - 1)) != 0) {
|
||||
fprintf(stderr, "region: base %p not 2 MiB aligned\n", (void*)base);
|
||||
VirtualFree(os_base, 0, MEM_RELEASE);
|
||||
return 1;
|
||||
}
|
||||
|
||||
out->os_base = os_base;
|
||||
out->base = base;
|
||||
out->os_total = os_total;
|
||||
out->producer = (vgpu_producer_t*)(base + VGPU_PRODUCER_OFFSET);
|
||||
out->control = (vgpu_control_t*)(base + VGPU_CONTROL_OFFSET);
|
||||
out->ring = base + VGPU_RING_OFFSET;
|
||||
|
||||
region_init_contract(out);
|
||||
|
||||
fprintf(stderr, "region: contract ready (producer=%p control=%p ring=%p)\n",
|
||||
(void*)out->producer, (void*)out->control, (void*)out->ring);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void vgpu_region_destroy(vgpu_region_t* r) {
|
||||
if (r && r->os_base) {
|
||||
VirtualUnlock(r->base, (SIZE_T)VGPU_REGION_BYTES);
|
||||
VirtualFree(r->os_base, 0, MEM_RELEASE);
|
||||
memset(r, 0, sizeof *r);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef VGPU_REGION_H
|
||||
#define VGPU_REGION_H
|
||||
|
||||
/* region.h — win32 pinned contract region (resolves blocks for the region-view). */
|
||||
|
||||
#include <stdint.h>
|
||||
#include "vgpu_stream.h" /* public contract: blocks, offsets, slot geometry */
|
||||
|
||||
/*
|
||||
* One contiguous 2 MiB-aligned pinned region holding the full contract:
|
||||
* producer block (page 0), control block (page 1), then SLOT_COUNT frame slots
|
||||
* starting at VGPU_RING_OFFSET. Object = memory: the region owns the mapping,
|
||||
* its lifetime is the mapping's lifetime. No hidden global state.
|
||||
*/
|
||||
typedef struct {
|
||||
void* os_base; /* raw allocation base (for free) */
|
||||
uint8_t* base; /* 2 MiB-aligned region base (== contract origin) */
|
||||
uint64_t os_total; /* bytes reserved at os_base */
|
||||
vgpu_producer_t* producer; /* base + VGPU_PRODUCER_OFFSET */
|
||||
vgpu_control_t* control; /* base + VGPU_CONTROL_OFFSET */
|
||||
uint8_t* ring; /* base + VGPU_RING_OFFSET */
|
||||
} vgpu_region_t;
|
||||
|
||||
/* Returns 0 on success, non-zero on failure (region zeroed on failure). */
|
||||
int vgpu_region_create(vgpu_region_t* out);
|
||||
void vgpu_region_destroy(vgpu_region_t* r);
|
||||
|
||||
#endif /* VGPU_REGION_H */
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
FlatView #0
|
||||
AS "cpu-smm-0", root: mem-container-smram
|
||||
Root memory region: mem-container-smram
|
||||
0000000000000000-0000000000017fff (prio 0, ram): ram0
|
||||
0000000000018000-0000000000018fff (prio 0, ram): synic-0-msg-page
|
||||
000000000001c000-000000007fffffff (prio 0, ram): ram0 @000000000001c000
|
||||
0000000080000000-0000000081ffffff (prio 0, i/o): vfio-pci-bar3
|
||||
0000000100000000-000000017fffffff (prio 0, ram): ram0 @0000000040000000
|
||||
|
||||
FlatView #1
|
||||
AS "I/O", root: io
|
||||
Root memory region: io
|
||||
0000000000000000-0000000000000007 (prio 0, i/o): dma-chan
|
||||
0000000000000060-0000000000000060 (prio 0, i/o): i8042-data
|
||||
0000000000000064-0000000000000064 (prio 0, i/o): i8042-cmd
|
||||
|
||||
FlatView #2
|
||||
AS "memory", root: system
|
||||
AS "cpu-memory-0", root: system
|
||||
Root memory region: system
|
||||
0000000000000000-0000000000017fff (prio 0, ram): ram0
|
||||
0000000000018000-0000000000018fff (prio 0, ram): synic-0-msg-page
|
||||
0000000000019000-0000000000019fff (prio 0, ram): synic-1-msg-page
|
||||
000000000001a000-000000000001afff (prio 0, ram): synic-2-msg-page
|
||||
000000000001b000-000000000001bfff (prio 0, ram): synic-3-msg-page
|
||||
000000000001c000-000000000002ffff (prio 0, ram): ram0 @000000000001c000
|
||||
0000000000030000-000000000004ffff (prio 1, i/o): smbase-blackhole
|
||||
0000000000050000-00000000000bffff (prio 0, ram): ram0 @0000000000050000
|
||||
00000000000c0000-00000000000dffff (prio 1, rom): pc.rom
|
||||
00000000000e0000-00000000000fffff (prio 0, rom): system.flash0 @000000000035c000
|
||||
0000000000100000-000000007bffffff (prio 0, ram): ram0 @0000000000100000
|
||||
000000007c000000-000000007fffffff (prio 1, i/o): tseg-blackhole
|
||||
0000000080000000-0000000081ffffff (prio 0, i/o): vfio-pci-bar3
|
||||
0000000082000000-0000000082087fff (prio 0, i/o): vfio-pci-bar0
|
||||
00000000e0000000-00000000efffffff (prio 0, i/o): pcie-mmcfg-mmio
|
||||
00000000fec00000-00000000fec00fff (prio 0, i/o): kvm-ioapic
|
||||
00000000ffc00000-00000000ffc83fff (prio 0, romd): system.flash1
|
||||
0000000100000000-000000027fffffff (prio 0, ram): ram0 @0000000080000000
|
||||
|
||||
FlatView #3
|
||||
AS "pci_bridge_io", root: pci_bridge_io
|
||||
Root memory region: pci_bridge_io
|
||||
+165
-1
@@ -42,7 +42,7 @@ struct holder {
|
||||
holder* peer; /* multi-VM: stop when both are ready (or NULL) */
|
||||
int is_driver; /* stops the loop on a condition */
|
||||
uint32_t expect_ep;
|
||||
int memctx, invalidated, ticks, bad_ep;
|
||||
int memctx, invalidated, ticks, bad_ep, errors;
|
||||
uint64_t last_kcr3, kcr3_e0;
|
||||
uint32_t last_epoch, last_nseg;
|
||||
int ro_ok, rw_eacces, seg0_ok;
|
||||
@@ -66,6 +66,7 @@ static int h_on_ev(void* u, const vmsig_event* ev) {
|
||||
holder* h = u;
|
||||
if (ev->kind == VMSIG_EV_VM_LIFECYCLE) h->ticks++;
|
||||
else if (ev->kind == VMSIG_EV_MEMCTX_INVALIDATED) h->invalidated++;
|
||||
else if (ev->kind == VMSIG_EV_ERROR) h->errors++; /* no boot-retry ERROR spam */
|
||||
maybe_stop(h);
|
||||
return 0;
|
||||
}
|
||||
@@ -393,6 +394,164 @@ static void test_ro_fd_ownership(void) {
|
||||
if (fcntl(ro, F_GETFD) >= 0) close(ro); /* belt-and-braces if the assert failed */
|
||||
}
|
||||
|
||||
/* ---- 7. cold-bootstrap retry: stub fails N times, then publishes via backoff ----- *
|
||||
* Regression for the cold-bootstrap-while-guest-boots bug: a failed bootstrap must NOT be
|
||||
* terminal nor emit URGENT ERROR — it arms a one-shot backoff timerfd that re-kicks the
|
||||
* bootstrap until it succeeds. fail_boots=3 makes the first three stub bootstraps fail
|
||||
* deterministically (no timing dependence); the real timerfd fires at ~50/100/200ms, so the
|
||||
* 4th kick succeeds sub-second. vmhost is added (as test_multicast) for the ticks failsafe
|
||||
* and a realistic loop; stop on memctx>=1 (stop_epoch=-1). */
|
||||
static void test_retry(void) {
|
||||
printf("test_retry\n");
|
||||
vmsig_ctx* ctx = vmsig_ctx_new();
|
||||
vmsig_core* core = vmsig_core_new(ctx);
|
||||
|
||||
holder h; memset(&h, 0, sizeof h);
|
||||
h.core = core; h.is_driver = 1; h.expect_ep = 0; h.stop_epoch = -1;
|
||||
/* OBSERVE so vmhost lifecycle ticks reach maybe_stop (ticks>30 failsafe) and ERROR
|
||||
* (if any) is counted; MEMCTX cap to receive the published context. */
|
||||
add_holder(core, &h, VMSIG_CAP_MEMCTX | VMSIG_CAP_OBSERVE, 0xFFFFFFFFu, 1ull << 0);
|
||||
|
||||
CHECK(vmsig_core_add_adapter(core, vmsig_vmhost_ops(), NULL, 0) >= 0, "add vmhost (watchdog)");
|
||||
|
||||
vmsig_memctx_cfg mc; memset(&mc, 0, sizeof mc);
|
||||
mc.stub = 1; mc.ram_path = NULL; mc.low = 0; mc.ro_fd = -1; mc.fail_boots = 3;
|
||||
CHECK(vmsig_core_add_adapter(core, vmsig_memctx_ops(), &mc, 0) >= 0, "add memctx (fail_boots=3)");
|
||||
|
||||
vmsig_core_run(core);
|
||||
|
||||
CHECK(h.memctx >= 1, "MEMCTX published after a series of bootstrap failures (retry worked)");
|
||||
CHECK(h.last_kcr3 != 0, "valid kcr3 after the successful retry");
|
||||
CHECK(h.errors == 0, "no ERROR spam during boot retries");
|
||||
|
||||
vmsig_core_free(core);
|
||||
vmsig_ctx_free(ctx);
|
||||
}
|
||||
|
||||
/* ---- 8-11. kcr3-persist MECHANICS (stub) ---------------------------------- *
|
||||
* These exercise the persist MACHINERY only: save/load, corruption fail-soft, drop-on-
|
||||
* invalidate, and the fast-vs-slow path selection. They do NOT exercise the real boot-session
|
||||
* validation (vmie_win32_open_ro_fd rejecting a stale kcr3) — that is VMIE-dependent and is
|
||||
* covered only on the armed stand. Under the stub, MC_JOB_RESUME synthetically ACCEPTS any
|
||||
* nonzero kcr3 (there is no live RAM to validate against), so a successful RESUME here proves
|
||||
* the mechanism wired the cached kcr3 into a publication, NOT that the kcr3 was validated. */
|
||||
|
||||
static int file_exists(const char* path) { return access(path, F_OK) == 0; }
|
||||
|
||||
/* Run a memctx endpoint to its first MEMCTX (or the ticks failsafe) over a private core. */
|
||||
static void run_once(uint64_t* out_kcr3, int* out_memctx, const char* persist_path,
|
||||
uint32_t fail_boots) {
|
||||
vmsig_ctx* ctx = vmsig_ctx_new();
|
||||
vmsig_core* core = vmsig_core_new(ctx);
|
||||
|
||||
holder h; memset(&h, 0, sizeof h);
|
||||
h.core = core; h.is_driver = 1; h.expect_ep = 0; h.stop_epoch = -1;
|
||||
add_holder(core, &h, VMSIG_CAP_MEMCTX | VMSIG_CAP_OBSERVE, 0xFFFFFFFFu, 1ull << 0);
|
||||
|
||||
CHECK(vmsig_core_add_adapter(core, vmsig_vmhost_ops(), NULL, 0) >= 0, "add vmhost (watchdog)");
|
||||
|
||||
vmsig_memctx_cfg mc; memset(&mc, 0, sizeof mc);
|
||||
mc.stub = 1; mc.ram_path = NULL; mc.low = 0; mc.ro_fd = -1;
|
||||
mc.fail_boots = fail_boots; mc.persist_path = persist_path;
|
||||
CHECK(vmsig_core_add_adapter(core, vmsig_memctx_ops(), &mc, 0) >= 0, "add memctx");
|
||||
|
||||
vmsig_core_run(core);
|
||||
if (out_kcr3) *out_kcr3 = h.last_kcr3;
|
||||
if (out_memctx) *out_memctx = h.memctx;
|
||||
|
||||
vmsig_core_free(core);
|
||||
vmsig_ctx_free(ctx);
|
||||
}
|
||||
|
||||
/* 8. save-then-resume: run1 (cold stub bootstrap) publishes MEMCTX and WRITES the cache; run2
|
||||
* over the SAME persist_path takes the RESUME fast-path. The KEY is fail_boots=large in run2:
|
||||
* if it had gone through a cold bootstrap it would have failed N times (no MEMCTX inside the
|
||||
* loop budget); a prompt MEMCTX carrying the SAVED kcr3 proves RESUME bypassed the bootstrap. */
|
||||
static void test_persist_save_then_resume(void) {
|
||||
printf("test_persist_save_then_resume\n");
|
||||
char path[256];
|
||||
snprintf(path, sizeof path, "/tmp/vmsig-kcrx-%d.bin", (int)getpid());
|
||||
unlink(path);
|
||||
|
||||
uint64_t k1 = 0; int m1 = 0;
|
||||
run_once(&k1, &m1, path, 0);
|
||||
CHECK(m1 >= 1, "run1 published MEMCTX");
|
||||
CHECK(k1 != 0, "run1 kcr3 nonzero");
|
||||
CHECK(file_exists(path), "run1 wrote the kcr3 cache file");
|
||||
|
||||
/* run2: a cold bootstrap would fail 1000 times — only RESUME can publish promptly. */
|
||||
uint64_t k2 = 0; int m2 = 0;
|
||||
run_once(&k2, &m2, path, 1000);
|
||||
CHECK(m2 >= 1, "run2 published MEMCTX via the RESUME fast-path (bootstrap would have failed)");
|
||||
CHECK(k2 == k1, "run2 published the SAVED kcr3 (resumed from cache, not a fresh scan)");
|
||||
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
/* 9. corrupt file => load fail-soft => cold bootstrap still brings the context up. */
|
||||
static void test_persist_corrupt(void) {
|
||||
printf("test_persist_corrupt\n");
|
||||
char path[256];
|
||||
snprintf(path, sizeof path, "/tmp/vmsig-kcrx-corrupt-%d.bin", (int)getpid());
|
||||
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
CHECK(fd >= 0, "created a corrupt cache file");
|
||||
if (fd >= 0) { (void)!write(fd, "x", 1); close(fd); } /* 1 byte: short/wrong magic */
|
||||
|
||||
uint64_t k = 0; int m = 0;
|
||||
run_once(&k, &m, path, 0); /* load miss => cold bootstrap (fail_boots=0 => succeeds) */
|
||||
CHECK(m >= 1, "MEMCTX still published after a corrupt cache (fail-soft load)");
|
||||
CHECK(k != 0, "kcr3 nonzero from the cold bootstrap");
|
||||
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
/* 10. invalidate drops the cache; the re-bootstrap on the new epoch rewrites it fresh. */
|
||||
static void test_persist_invalidate_drop(void) {
|
||||
printf("test_persist_invalidate_drop\n");
|
||||
char path[256];
|
||||
snprintf(path, sizeof path, "/tmp/vmsig-kcrx-inv-%d.bin", (int)getpid());
|
||||
unlink(path);
|
||||
|
||||
vmsig_ctx* ctx = vmsig_ctx_new();
|
||||
vmsig_core* core = vmsig_core_new(ctx);
|
||||
|
||||
holder h; memset(&h, 0, sizeof h);
|
||||
/* inject a destructive lifecycle on epoch0 (as test_epoch); stop after epoch1. */
|
||||
h.core = core; h.is_driver = 1; h.expect_ep = 0; h.inject_reset = 1; h.stop_epoch = 1;
|
||||
add_holder(core, &h, VMSIG_CAP_MEMCTX | VMSIG_CAP_OBSERVE, 0xFFFFFFFFu, 1ull << 0);
|
||||
|
||||
CHECK(vmsig_core_add_adapter(core, vmsig_vmhost_ops(), NULL, 0) >= 0, "add vmhost (watchdog)");
|
||||
vmsig_memctx_cfg mc; memset(&mc, 0, sizeof mc);
|
||||
mc.stub = 1; mc.ram_path = NULL; mc.low = 0; mc.ro_fd = -1; mc.persist_path = path;
|
||||
CHECK(vmsig_core_add_adapter(core, vmsig_memctx_ops(), &mc, 0) >= 0, "add memctx");
|
||||
|
||||
vmsig_core_run(core);
|
||||
|
||||
/* epoch0 bootstrap wrote the cache; invalidate dropped it; epoch1 bootstrap rewrote it. */
|
||||
CHECK(h.invalidated >= 1, "invalidation fired");
|
||||
CHECK(h.last_epoch == 1, "re-published at epoch 1 after invalidate");
|
||||
CHECK(file_exists(path), "cache rewritten by the post-invalidate bootstrap");
|
||||
|
||||
vmsig_core_free(core);
|
||||
vmsig_ctx_free(ctx);
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
/* 11. persist disabled (persist_path=NULL): no cache file is ever created (today's behavior). */
|
||||
static void test_persist_stub_disabled(void) {
|
||||
printf("test_persist_stub_disabled\n");
|
||||
char path[256];
|
||||
snprintf(path, sizeof path, "/tmp/vmsig-kcrx-off-%d.bin", (int)getpid());
|
||||
unlink(path);
|
||||
|
||||
uint64_t k = 0; int m = 0;
|
||||
run_once(&k, &m, NULL, 0); /* persist off */
|
||||
CHECK(m >= 1, "MEMCTX published with persist disabled");
|
||||
CHECK(!file_exists(path), "no cache file created when persist is disabled");
|
||||
|
||||
unlink(path); /* belt-and-braces */
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_multicast();
|
||||
test_epoch();
|
||||
@@ -400,6 +559,11 @@ int main(void) {
|
||||
test_multivm();
|
||||
test_socket();
|
||||
test_ro_fd_ownership();
|
||||
test_retry();
|
||||
test_persist_save_then_resume();
|
||||
test_persist_corrupt();
|
||||
test_persist_invalidate_drop();
|
||||
test_persist_stub_disabled();
|
||||
printf("memctx tests: %s\n", g_fail ? "FAIL" : "PASS");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/* test_mtree.c — unit tests for mtree_low_split (the below-4G split parser). Pure text in,
|
||||
* number out; no QMP/transport. The fragmented fixture reproduces the structural traps the
|
||||
* old heuristic tripped on (Hyper-V synic overlays, smbase/tseg blackhole holes, rom holes)
|
||||
* plus a decoy non-system flatview that carries its OWN GPA-0 stub and a DIFFERENT @offset,
|
||||
* proving the system address space is selected (not "first match in the text"). */
|
||||
#define _GNU_SOURCE
|
||||
#include "mtree.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifndef FIXTURE_DIR
|
||||
#define FIXTURE_DIR "."
|
||||
#endif
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond, msg) do { if (!(cond)) { printf(" FAIL: %s\n", (msg)); g_fail = 1; } } while (0)
|
||||
|
||||
/* Slurp a whole text file into a heap buffer (NUL-terminated). NULL on error. */
|
||||
static char* slurp(const char* path) {
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (!f) return NULL;
|
||||
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; }
|
||||
long sz = ftell(f);
|
||||
if (sz < 0) { fclose(f); return NULL; }
|
||||
rewind(f);
|
||||
char* buf = malloc((size_t)sz + 1);
|
||||
if (!buf) { fclose(f); return NULL; }
|
||||
size_t got = fread(buf, 1, (size_t)sz, f);
|
||||
fclose(f);
|
||||
buf[got] = 0;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Re-encode every '\n' as '\r\n' (QEMU's HMP output is CRLF). Caller frees; NULL on OOM. */
|
||||
static char* to_crlf(const char* lf) {
|
||||
size_t n = 0, extra = 0;
|
||||
for (const char* p = lf; *p; p++) { n++; if (*p == '\n') extra++; }
|
||||
char* out = malloc(n + extra + 1);
|
||||
if (!out) return NULL;
|
||||
char* o = out;
|
||||
for (const char* p = lf; *p; p++) { if (*p == '\n') *o++ = '\r'; *o++ = *p; }
|
||||
*o = 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Case B: a minimal, NON-fragmented system flatview — one big GPA-0 ram run plus high-RAM
|
||||
* carrying @<low>. Must not be broken by the new parser. */
|
||||
static const char* k_happy =
|
||||
"FlatView #0\n"
|
||||
" AS \"memory\", root: system\n"
|
||||
" Root memory region: system\n"
|
||||
" 0000000000000000-000000007fffffff (prio 0, ram): ram0\n"
|
||||
" 0000000080000000-0000000081ffffff (prio 0, i/o): vfio-pci-bar3\n"
|
||||
" 0000000100000000-000000017fffffff (prio 0, ram): ram0 @0000000080000000\n";
|
||||
|
||||
/* Case C: text without any system flatview => fail-closed. */
|
||||
static const char* k_no_system =
|
||||
"FlatView #0\n"
|
||||
" AS \"I/O\", root: io\n"
|
||||
" Root memory region: io\n"
|
||||
" 0000000000000000-0000000000000007 (prio 0, i/o): dma-chan\n";
|
||||
|
||||
int main(void) {
|
||||
printf("test_mtree\n");
|
||||
|
||||
/* Cases A and E: the fragmented fixture (decoy first, system second). */
|
||||
char path[1024];
|
||||
snprintf(path, sizeof path, "%s/mtree_split_fragmented.txt", FIXTURE_DIR);
|
||||
char* frag = slurp(path);
|
||||
CHECK(frag != NULL, "fragmented fixture loaded");
|
||||
if (frag) {
|
||||
uint64_t low = mtree_low_split(frag);
|
||||
/* A: fragmented low-RAM must NOT yield the GPA-0 stub end (0x18000) — the bug. */
|
||||
CHECK(low == 0x80000000ull, "A: fragmented split == 0x80000000");
|
||||
CHECK(low != 0x18000ull, "A: not the GPA-0 stub end (0x18000)");
|
||||
/* E: the decoy (non-system) flatview comes FIRST and carries @0x40000000; the
|
||||
* function must select the SYSTEM flatview (@0x80000000), not the decoy. */
|
||||
CHECK(low != 0x40000000ull, "E: decoy flatview @offset rejected (system AS chosen)");
|
||||
/* F: real QEMU HMP output is CRLF. The parser MUST tolerate '\r' — a synthetic
|
||||
* LF-only fixture hid this, so the shipped parser returned 0 on the real VM mtree
|
||||
* (-> low=0 -> VM never attached). Regression guard, independent of how git stores
|
||||
* the fixture's line endings. */
|
||||
char* frag_crlf = to_crlf(frag);
|
||||
CHECK(frag_crlf != NULL, "F: CRLF copy allocated");
|
||||
if (frag_crlf) {
|
||||
CHECK(mtree_low_split(frag_crlf) == 0x80000000ull, "F: CRLF fragmented split == 0x80000000");
|
||||
free(frag_crlf);
|
||||
}
|
||||
free(frag);
|
||||
}
|
||||
|
||||
/* Case B: happy path (non-fragmented) still resolves to the high-RAM @offset. */
|
||||
CHECK(mtree_low_split(k_happy) == 0x80000000ull, "B: non-fragmented happy path == 0x80000000");
|
||||
|
||||
/* Case C: no system flatview => 0. */
|
||||
CHECK(mtree_low_split(k_no_system) == 0, "C: no system flatview => fail-closed 0");
|
||||
|
||||
/* Case D: garbage / empty => 0. */
|
||||
CHECK(mtree_low_split("") == 0, "D: empty text => 0");
|
||||
CHECK(mtree_low_split("not an mtree at all\n") == 0, "D: junk text => 0");
|
||||
|
||||
printf("mtree tests: %s\n", g_fail ? "FAIL" : "PASS");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/* test_uinputlayout.c — DECLARATIVE uinput capability split (pure, no /dev/uinput).
|
||||
*
|
||||
* Verifies the CONSTANT A/B role mapping that drives both device creation and the hot-path
|
||||
* button/wheel carrier selection: device A = keyboard only, device B = relative pointer + buttons
|
||||
* + wheel, and the button/wheel carrier is B. There is no absolute pointer anywhere — the abs role
|
||||
* has been removed and is unrepresentable (no abs field exists in uinput_role). The actuation
|
||||
* ioctls remain armed-only (they need a real /dev/uinput); this covers the layout logic. */
|
||||
#include "vmctl.h"
|
||||
#include "uinput_layout.h"
|
||||
#include <stdio.h>
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (!(cond)) { printf(" FAIL: %s\n", (msg)); g_fail = 1; } \
|
||||
} while (0)
|
||||
|
||||
int main(void) {
|
||||
uinput_role a, b; int btn_on_b;
|
||||
|
||||
/* Constant layout: A = keyboard only (no pointer, no buttons/wheel); B = relative pointer +
|
||||
* buttons + wheel; the button/wheel carrier is B. */
|
||||
vmctl_uinput_layout(&a, &b, &btn_on_b);
|
||||
CHECK(a.present && b.present, "two devices");
|
||||
CHECK(a.want_keyboard, "A has keyboard");
|
||||
CHECK(!a.rel_motion, "A has no pointer (keyboard-only)");
|
||||
CHECK(!a.want_buttons, "A has NO mouse buttons");
|
||||
CHECK(!a.want_wheel, "A has NO wheel");
|
||||
CHECK(!b.want_keyboard, "B has no keyboard");
|
||||
CHECK(b.rel_motion, "B is relative");
|
||||
CHECK(b.want_buttons, "B has mouse buttons");
|
||||
CHECK(b.want_wheel, "B has wheel");
|
||||
CHECK(btn_on_b == 1, "button/wheel carrier is B");
|
||||
|
||||
/* No absolute pointer: the abs role is removed and unrepresentable (uinput_role carries no abs
|
||||
* field). The invariant is that each device is either relative or has no pointer at all — A is
|
||||
* keyboard-only (no pointer), B is relative. Neither advertises an absolute axis. */
|
||||
CHECK(!a.rel_motion && !a.want_buttons && !a.want_wheel, "A is keyboard-only (no pointer)");
|
||||
CHECK(b.rel_motion, "B is the relative pointer (not absolute)");
|
||||
|
||||
/* evdev export contract: a NULL handle reports "not a uinput handle" (-1). The populated
|
||||
* path (real /dev/input/eventN) is armed-only — it needs a created uinput device. */
|
||||
{
|
||||
char ea[64], eb[64];
|
||||
CHECK(vmctl_uinput_evdev(NULL, ea, eb) == -1, "evdev export: NULL handle -> -1");
|
||||
}
|
||||
|
||||
printf("uinputlayout tests: %s\n", g_fail ? "FAIL" : "PASS");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
+125
-13
@@ -1,7 +1,14 @@
|
||||
/* test_vmhost.c — QEMU/QMP host-plane, armed path: fake QMP server (this test)
|
||||
* <-> real QMP client vmhost. We verify: handshake (greeting -> qmp_capabilities
|
||||
* -> return -> SEAM_UP), async events -> VM_LIFECYCLE (broadcast), CMD_VM{QUERY}
|
||||
* -> command to server -> return -> addressed VM_LIFECYCLE to the initiator, EOF -> SEAM_DOWN. */
|
||||
* -> command to server -> return -> addressed VM_LIFECYCLE to the initiator, EOF -> SEAM_DOWN.
|
||||
*
|
||||
* It also verifies the host->guest input bridge: with bridge_evdev_a/b set in cfg, on reaching
|
||||
* READY the seam adds two input-linux objects (A with grab_all, B without) over its own
|
||||
* connection, with neutral per-endpoint ids and the evdev paths from cfg; each add is preceded
|
||||
* by an idempotent object-del of the same id (clears a stale object from a crashed/racing prior
|
||||
* daemon); the bridge replies never surface as ACK/VM_LIFECYCLE to control; on teardown it
|
||||
* fires object-del for both. */
|
||||
#define _GNU_SOURCE
|
||||
#include "vmsig.h"
|
||||
#include "vmhost.h" /* private cfg (CMake provides the include path) */
|
||||
@@ -25,6 +32,7 @@ static int g_fail = 0;
|
||||
|
||||
static atomic_int g_seamup = 0, g_seamdown = 0;
|
||||
static atomic_int g_paused = 0, g_running_bcast = 0, g_query_reply = 0;
|
||||
static atomic_int g_stray_ack = 0; /* any ACT_ACK reaching control would be a bridge leak */
|
||||
static void* g_ctl = NULL;
|
||||
|
||||
static int on_ev(void* user, const vmsig_event* ev) {
|
||||
@@ -38,6 +46,8 @@ static int on_ev(void* user, const vmsig_event* ev) {
|
||||
vmsig_inproc_send(g_ctl, &d);
|
||||
} else if (ev->kind == VMSIG_EV_SEAM_DOWN && ev->source == VMSIG_SRC_VMHOST) {
|
||||
atomic_store(&g_seamdown, 1);
|
||||
} else if (ev->kind == VMSIG_EV_ACT_ACK && ev->source == VMSIG_SRC_VMHOST) {
|
||||
atomic_fetch_add(&g_stray_ack, 1); /* bridge ops must NOT ack control */
|
||||
} else if (ev->kind == VMSIG_EV_VM_LIFECYCLE) {
|
||||
vmsig_vm_state vs; memcpy(&vs, ev->inln, sizeof vs);
|
||||
if (ev->origin) { /* addressed reply to our QUERY */
|
||||
@@ -64,16 +74,27 @@ static int srv_listen(const char* name) {
|
||||
return fd;
|
||||
}
|
||||
static void srv_send(int fd, const char* s) { ssize_t r = write(fd, s, strlen(s)); (void)r; }
|
||||
|
||||
/* Persistent accumulator: client traffic can interleave (e.g. bridge object_add on READY vs.
|
||||
* CMD_VM query on SEAM_UP), so we keep ALL received bytes and match substrings against the
|
||||
* cumulative text instead of per-call buffers. */
|
||||
static char g_rx[8192];
|
||||
static size_t g_rxlen = 0;
|
||||
static void rx_reset(void) { g_rxlen = 0; g_rx[0] = 0; }
|
||||
static void rx_pump(int fd) {
|
||||
ssize_t r = read(fd, g_rx + g_rxlen, sizeof g_rx - 1 - g_rxlen);
|
||||
if (r > 0) { g_rxlen += (size_t)r; g_rx[g_rxlen] = 0; }
|
||||
}
|
||||
/* Wait until the cumulative client traffic contains needle (or timeout ~2s). */
|
||||
static int srv_expect(int fd, const char* needle) {
|
||||
char buf[1024]; size_t len = 0;
|
||||
for (int i = 0; i < 200; i++) { /* up to ~2s */
|
||||
ssize_t r = read(fd, buf + len, sizeof buf - 1 - len);
|
||||
if (r > 0) { len += (size_t)r; buf[len] = 0; if (strstr(buf, needle)) return 1; }
|
||||
else if (r == 0) return 0;
|
||||
else { struct timespec t = { 0, 10 * 1000000 }; nanosleep(&t, NULL); }
|
||||
if (len >= sizeof buf - 1) len = 0;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
if (strstr(g_rx, needle)) return 1;
|
||||
rx_pump(fd);
|
||||
if (strstr(g_rx, needle)) return 1;
|
||||
struct timespec t = { 0, 10 * 1000000 }; nanosleep(&t, NULL);
|
||||
if (g_rxlen >= sizeof g_rx - 1) return strstr(g_rx, needle) != NULL; /* full: stop growing */
|
||||
}
|
||||
return 0;
|
||||
return strstr(g_rx, needle) != NULL;
|
||||
}
|
||||
static void wait_atomic(atomic_int* a, int ms) {
|
||||
for (int i = 0; i < ms; i++) {
|
||||
@@ -98,10 +119,16 @@ int main(void) {
|
||||
g.cap_mask = VMSIG_CAP_OBSERVE | VMSIG_CAP_VM;
|
||||
vmsig_core_add_control(core, vmsig_inproc_control_ops(), ctl, &g);
|
||||
|
||||
/* armed vmhost: it will connect to our fake QMP */
|
||||
vmsig_vmhost_cfg vcfg; memset(&vcfg, 0, sizeof vcfg); vcfg.qmp_path = QMP;
|
||||
/* armed vmhost: it will connect to our fake QMP. Bridge evdev paths set => on READY the
|
||||
* seam should add two input-linux objects forwarding them into the guest. */
|
||||
const char* EVDEV_A = "/dev/input/event42";
|
||||
const char* EVDEV_B = "/dev/input/event43";
|
||||
vmsig_vmhost_cfg vcfg; memset(&vcfg, 0, sizeof vcfg);
|
||||
vcfg.qmp_path = QMP; vcfg.bridge_evdev_a = EVDEV_A; vcfg.bridge_evdev_b = EVDEV_B;
|
||||
CHECK(vmsig_core_add_adapter(core, vmsig_vmhost_ops(), &vcfg, 0) >= 0, "vmhost armed attach");
|
||||
|
||||
rx_reset();
|
||||
|
||||
pthread_t th; pthread_create(&th, NULL, loop_main, core);
|
||||
|
||||
/* === QMP server role === */
|
||||
@@ -113,17 +140,45 @@ int main(void) {
|
||||
|
||||
srv_send(c, "{\"QMP\": {\"version\": {}, \"capabilities\": []}}\r\n");
|
||||
CHECK(srv_expect(c, "qmp_capabilities"), "client sent qmp_capabilities");
|
||||
srv_send(c, "{\"return\": {}}\r\n"); /* -> READY -> SEAM_UP */
|
||||
srv_send(c, "{\"return\": {}}\r\n"); /* -> READY -> SEAM_UP + bridge */
|
||||
|
||||
/* On READY the seam adds the input-linux bridge BEFORE the SEAM_UP-driven CMD_VM query
|
||||
* (bridge ids 1,2; query id 3). Verify both object_add lines and their properties. */
|
||||
CHECK(srv_expect(c, "object-add"), "seam sent object-add (input-linux bridge)");
|
||||
CHECK(srv_expect(c, "\"input-linux\""), "bridge object uses qom-type input-linux");
|
||||
CHECK(srv_expect(c, "\"vmsig-in-a-0\""), "bridge A has neutral per-endpoint id");
|
||||
CHECK(srv_expect(c, "\"vmsig-in-b-0\""), "bridge B has neutral per-endpoint id");
|
||||
CHECK(srv_expect(c, EVDEV_A), "bridge A carries the cfg evdev path for A");
|
||||
CHECK(srv_expect(c, EVDEV_B), "bridge B carries the cfg evdev path for B");
|
||||
/* Idempotent re-attach: each add is preceded by an object-del of the same id. The EOF
|
||||
* teardown below skips del (seam DEAD), so this object-del can ONLY originate from the
|
||||
* del-before-add path. */
|
||||
CHECK(srv_expect(c, "object-del"), "bridge fires object-del before add (idempotent re-attach)");
|
||||
srv_send(c, "{\"return\": {}, \"id\": 1}\r\n"); /* ack bridge A (consumed silently) */
|
||||
srv_send(c, "{\"return\": {}, \"id\": 2}\r\n"); /* ack bridge B (consumed silently) */
|
||||
|
||||
/* grab_all must be on A only: it appears exactly once, and only A's add carries it. The
|
||||
* accumulator holds A's line ending in grab_all before B's id; assert B's add has none
|
||||
* by checking grab_all precedes "vmsig-in-b-0" in the stream and never reappears. */
|
||||
{
|
||||
const char* g1 = strstr(g_rx, "grab_all");
|
||||
const char* g2 = g1 ? strstr(g1 + 1, "grab_all") : NULL;
|
||||
const char* bid = strstr(g_rx, "vmsig-in-b-0");
|
||||
CHECK(g1 != NULL, "grab_all present (on A)");
|
||||
CHECK(g2 == NULL, "grab_all appears exactly once (A only)");
|
||||
CHECK(g1 && bid && g1 < bid, "grab_all belongs to A's add, not B's");
|
||||
}
|
||||
|
||||
srv_send(c, "{\"event\": \"STOP\"}\r\n"); /* -> broadcast PAUSED */
|
||||
CHECK(srv_expect(c, "query-status"), "client sent query-status (from CMD_VM)");
|
||||
srv_send(c, "{\"return\": {\"status\": \"running\"}, \"id\": 1}\r\n"); /* -> addressed reply */
|
||||
srv_send(c, "{\"return\": {\"status\": \"running\"}, \"id\": 3}\r\n"); /* -> addressed reply */
|
||||
srv_send(c, "{\"event\": \"RESUME\"}\r\n"); /* -> broadcast RUNNING */
|
||||
|
||||
wait_atomic(&g_seamup, 1000);
|
||||
wait_atomic(&g_paused, 1000);
|
||||
wait_atomic(&g_query_reply, 1000);
|
||||
wait_atomic(&g_running_bcast, 1000);
|
||||
CHECK(atomic_load(&g_stray_ack) == 0, "bridge ops did not leak ACK to control");
|
||||
|
||||
close(c); /* EOF -> SEAM_DOWN */
|
||||
wait_atomic(&g_seamdown, 1000);
|
||||
@@ -139,6 +194,63 @@ int main(void) {
|
||||
pthread_join(th, NULL);
|
||||
vmsig_core_free(core);
|
||||
vmsig_ctx_free(ctx);
|
||||
|
||||
/* === Scenario 2: object_del on a clean reap (connection still READY) ===
|
||||
* The EOF path above marks the seam DEAD, so its best-effort del is (correctly) skipped.
|
||||
* Here we reach READY then free the core WITHOUT EOF: vh_close must fire object_del for
|
||||
* both bridge ids over the still-open socket before closing its fd. */
|
||||
{
|
||||
const char* QMP2 = "@vmsig-qmp-fake-test-2";
|
||||
int srv2 = srv_listen(QMP2);
|
||||
CHECK(srv2 >= 0, "scenario2: srv_listen");
|
||||
if (srv2 >= 0) {
|
||||
vmsig_ctx* ctx2 = vmsig_ctx_new();
|
||||
vmsig_core* core2 = vmsig_core_new(ctx2);
|
||||
vmsig_vmhost_cfg vc2; memset(&vc2, 0, sizeof vc2);
|
||||
vc2.qmp_path = QMP2; vc2.bridge_evdev_a = EVDEV_A; vc2.bridge_evdev_b = EVDEV_B;
|
||||
CHECK(vmsig_core_add_adapter(core2, vmsig_vmhost_ops(), &vc2, 0) >= 0,
|
||||
"scenario2: vmhost attach");
|
||||
rx_reset();
|
||||
pthread_t th2; pthread_create(&th2, NULL, loop_main, core2);
|
||||
|
||||
int c2 = accept(srv2, NULL, NULL);
|
||||
CHECK(c2 >= 0, "scenario2: accept");
|
||||
if (c2 >= 0) {
|
||||
struct timeval tv2 = { 0, 50 * 1000 };
|
||||
setsockopt(c2, SOL_SOCKET, SO_RCVTIMEO, &tv2, sizeof tv2);
|
||||
srv_send(c2, "{\"QMP\": {\"version\": {}, \"capabilities\": []}}\r\n");
|
||||
CHECK(srv_expect(c2, "qmp_capabilities"), "scenario2: qmp_capabilities");
|
||||
srv_send(c2, "{\"return\": {}}\r\n"); /* READY -> bridge add */
|
||||
CHECK(srv_expect(c2, "vmsig-in-b-0"), "scenario2: bridge added");
|
||||
srv_send(c2, "{\"return\": {}, \"id\": 1}\r\n");
|
||||
srv_send(c2, "{\"return\": {}, \"id\": 2}\r\n");
|
||||
|
||||
/* Attach already emitted object-del (del-before-add). Reset the accumulator so the
|
||||
* teardown del below is verified in ISOLATION, not satisfied by the attach del. */
|
||||
rx_reset();
|
||||
|
||||
/* Clean reap WITHOUT EOF: stop the loop then free (vh_close fires del). */
|
||||
vmsig_core_stop(core2);
|
||||
pthread_join(th2, NULL);
|
||||
vmsig_core_free(core2);
|
||||
|
||||
for (int i = 0; i < 50 && !strstr(g_rx, "object-del"); i++) {
|
||||
rx_pump(c2);
|
||||
struct timespec t = { 0, 10 * 1000000 }; nanosleep(&t, NULL);
|
||||
}
|
||||
const char* d = strstr(g_rx, "object-del");
|
||||
CHECK(d != NULL, "scenario2: teardown fired object-del");
|
||||
CHECK(strstr(g_rx, "vmsig-in-a-0"), "scenario2: object_del for bridge A");
|
||||
CHECK(strstr(g_rx, "vmsig-in-b-0"), "scenario2: object_del for bridge B");
|
||||
close(c2);
|
||||
} else {
|
||||
vmsig_core_stop(core2); pthread_join(th2, NULL); vmsig_core_free(core2);
|
||||
}
|
||||
vmsig_ctx_free(ctx2);
|
||||
close(srv2);
|
||||
}
|
||||
}
|
||||
|
||||
close(srv);
|
||||
|
||||
printf("vmhost tests: %s\n", g_fail ? "FAIL" : "PASS");
|
||||
|
||||
Vendored
+275
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* \file This file contains definitions for NVFBC API.
|
||||
* \copyright
|
||||
*
|
||||
* Copyright 1993-2016 NVIDIA Corporation. All rights reserved.
|
||||
* NOTICE TO LICENSEE: This source code and/or documentation ("Licensed Deliverables")
|
||||
* are subject to the applicable NVIDIA license agreement
|
||||
* that governs the use of the Licensed Deliverables.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <Windows.h>
|
||||
|
||||
typedef unsigned char NvU8;
|
||||
typedef unsigned long NvU32;
|
||||
typedef unsigned long long NvU64;
|
||||
|
||||
/**
|
||||
* \defgroup NVFBC The NVIDIA Frame Buffer Capture API.
|
||||
* \brief Defines a set of interfaces for high performance Capture of desktop content.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \defgroup NVFBC_ENUMS Enums
|
||||
* \ingroup NVFBC
|
||||
* \brief Enumerations to be used with NVFBC API
|
||||
*/
|
||||
|
||||
/**
|
||||
* \defgroup NVFBC_STRUCTS Structs
|
||||
* \ingroup NVFBC
|
||||
* \brief Defines Parameter Structures to be used with NVFBC APIs.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \defgroup NVFBC_ENTRYPOINTS Entrypoints
|
||||
* \ingroup NVFBC
|
||||
* \brief Declarations for NVFBC Entrypoint functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC
|
||||
* Macro to define the NVFBC API version corresponding to this distribution.
|
||||
*/
|
||||
#define NVFBC_DLL_VERSION 0x50
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC
|
||||
* Macro to construct version numbers for parameter structs.
|
||||
*/
|
||||
#define NVFBC_STRUCT_VERSION(typeName, ver) (NvU32)(sizeof(typeName) | ((ver)<<16) | (NVFBC_DLL_VERSION << 24))
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC
|
||||
* Calling Convention
|
||||
*/
|
||||
#define NVFBCAPI __stdcall
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC
|
||||
* Indicates that there are no global overrides specified for NVFBC. To be used with NVFBC_SetGlobalFlags API
|
||||
*/
|
||||
#define NVFBC_GLOBAL_FLAGS_NONE 0x00000000
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC
|
||||
* Indicates to NVFBC that stereo rendering is enabled. Currently unsupported. To be used with NVFBC_SetGlobalFlags API.
|
||||
*/
|
||||
#define NVFBC_GLOBAL_FLAGS_STEREO_BUFFER 0x00000001
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC
|
||||
* Indicates that NVFBC should not request a repaint of the desktop when initiating NVFBC capture. To be used with NVFBC_SetGlobalFlags API.
|
||||
*/
|
||||
#define NVFBC_GLOBAL_FLAGS_NO_INITIAL_REFRESH 0x00000002
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC
|
||||
* Indicates that NVFBC should not reset the graphics driver while servicing subsequent NVFBC_Enable API requests.
|
||||
*/
|
||||
|
||||
#define NVFBC_GLOBAL_FLAGS_NO_DEVICE_RESET_TOGGLE 0x00000004
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_ENUMS
|
||||
* \brief Enumerates status codes returned by NVFBC APIs.
|
||||
*/
|
||||
typedef enum _NVFBCRESULT
|
||||
{
|
||||
NVFBC_SUCCESS = 0,
|
||||
NVFBC_ERROR_GENERIC = -1, /**< Unexpected failure in NVFBC. */
|
||||
NVFBC_ERROR_INVALID_PARAM = -2, /**< One or more of the paramteres passed to NvFBC are invalid [This include NULL pointers]. */
|
||||
NVFBC_ERROR_INVALIDATED_SESSION = -3, /**< NvFBC session is invalid. Client needs to recreate session. */
|
||||
NVFBC_ERROR_PROTECTED_CONTENT = -4, /**< Protected content detected. Capture failed. */
|
||||
NVFBC_ERROR_DRIVER_FAILURE = -5, /**< GPU driver returned failure to process NvFBC command. */
|
||||
NVFBC_ERROR_CUDA_FAILURE = -6, /**< CUDA driver returned failure to process NvFBC command. */
|
||||
NVFBC_ERROR_UNSUPPORTED = -7, /**< API Unsupported on this version of NvFBC. */
|
||||
NVFBC_ERROR_HW_ENC_FAILURE = -8, /**< HW Encoder returned failure to process NVFBC command. */
|
||||
NVFBC_ERROR_INCOMPATIBLE_DRIVER = -9, /**< NVFBC is not compatible with this version of the GPU driver. */
|
||||
NVFBC_ERROR_UNSUPPORTED_PLATFORM = -10, /**< NVFBC is not supported on this platform. */
|
||||
NVFBC_ERROR_OUT_OF_MEMORY = -11, /**< Failed to allocate memory. */
|
||||
NVFBC_ERROR_INVALID_PTR = -12, /**< A NULL pointer was passed. */
|
||||
NVFBC_ERROR_INCOMPATIBLE_VERSION = -13, /**< An API was called with a parameter struct that has an incompatible version. Check dwVersion field of paramter struct. */
|
||||
NVFBC_ERROR_OPT_CAPTURE_FAILURE = -14, /**< Desktop Capture failed. */
|
||||
NVFBC_ERROR_INSUFFICIENT_PRIVILEGES = -15, /**< User doesn't have appropriate previlages. */
|
||||
NVFBC_ERROR_INVALID_CALL = -16, /**< NVFBC APIs called in wrong sequence. */
|
||||
NVFBC_ERROR_SYSTEM_ERROR = -17, /**< Win32 error. */
|
||||
NVFBC_ERROR_INVALID_TARGET = -18, /**< The target adapter idx can not be used for NVFBC capture. It may not correspond to an NVIDIA GPU, or may not be attached to desktop. */
|
||||
NVFBC_ERROR_DYNAMIC_DISABLE = -20, /**< NvFBC is dynamically disabled. Cannot continue to capture */
|
||||
} NVFBCRESULT;
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_ENUMS
|
||||
* \brief Enumerates NVFBC states. To be used with NvFBC_Enable API
|
||||
*/
|
||||
typedef enum _NVFBC_STATE
|
||||
{
|
||||
NVFBC_STATE_DISABLE = 0, /** Disables NvFBC. */
|
||||
NVFBC_STATE_ENABLE , /** Enables NvFBC. */
|
||||
NVFBC_STATE_LAST , /** Sentinel value. Shouldn't be used. */
|
||||
} NVFBC_STATE;
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_STRUCTS
|
||||
* \brief Defines parameters that describe the grabbed data, and provides detailed information about status of the NVFBC session.
|
||||
*/
|
||||
typedef struct _NvFBCFrameGrabInfo
|
||||
{
|
||||
DWORD dwWidth; /**< [out] Indicates the current width of captured buffer. */
|
||||
DWORD dwHeight; /**< [out] Indicates the current height of captured buffer. */
|
||||
DWORD dwBufferWidth; /**< [out] Indicates the current width of the pixel buffer(padded width). */
|
||||
DWORD dwReserved; /**< [in] Reserved, do not use. */
|
||||
BOOL bOverlayActive; /**< [out] Is set to 1 if overlay was active. */
|
||||
BOOL bMustRecreate; /**< [out] Is set to 1 if the compressor must call NvBFC_Create again. */
|
||||
BOOL bFirstBuffer; /**< [out] Is set to 1 is this was the first capture call, or first call after a desktop mode change.
|
||||
Relevant only for XOR and diff modes supported by NVFBCToSys interface. */
|
||||
BOOL bHWMouseVisible; /**< [out] Is set to 1 if HW cursor was enabled by OS at the time of the grab. */
|
||||
BOOL bProtectedContent; /**< [out] Is set to 1 if protected content was active (DXVA encryption Session). */
|
||||
DWORD dwDriverInternalError; /**< [out] Indicates the status code from lower layers. 0 or 0xFBCA11F9 indicates no error was returned. */
|
||||
BOOL bStereoOn; /**< [out] Is set to 1 if stereo was on. */
|
||||
BOOL bIGPUCapture; /**< [out] Is set to 1 if the captured frame is from iGPU. 0 if capture fails or if captured from dGPU*/
|
||||
DWORD dwSourcePID; /**< [out] Indicates which process caused the last screen update that got grabbed*/
|
||||
DWORD dwReserved3; /**< [in] Reserved, do not use. */
|
||||
NvU32 dwReserved2[13]; /**< [in] Resereved, should be set to 0. */
|
||||
} NvFBCFrameGrabInfo;
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_STRUCTS
|
||||
* \brief Deines the parameters to be used with NvFBC_GetStatusEx API
|
||||
*/
|
||||
typedef struct _NvFBCStatusEx
|
||||
{
|
||||
NvU32 dwVersion; /**< [in] Struct version. Set to NVFBC_STATUS_VER. */
|
||||
NvU32 bIsCapturePossible :1; /**< [out] Indicates if NvFBC feature is enabled. */
|
||||
NvU32 bCurrentlyCapturing:1; /**< [out] Indicates if NVFBC is currently capturing for the Adapter ordinal specified in dwAdapterIdx. */
|
||||
NvU32 bCanCreateNow :1; /**< [out] Deprecated. Do not use. */
|
||||
NvU32 bSupportMultiHead :1; /**< [out] MultiHead grab supported. */
|
||||
NvU32 bSupportMultiClient:1; /**< [out] Multiple capture clients on same display adapter supported. */
|
||||
NvU32 bReservedBits :27; /**< [in] Reserved, do not use. */
|
||||
NvU32 dwNvFBCVersion; /**< [out] Indicates the highest NvFBC interface version supported by the loaded NVFBC library. */
|
||||
NvU32 dwAdapterIdx; /**< [in] Adapter Ordinal corresponding to the display to be grabbed. IGNORED if bCapturePID is set */
|
||||
void* pPrivateData; /**< [in] optional **/
|
||||
NvU32 dwPrivateDataSize; /**< [in] optional **/
|
||||
NvU32 dwReserved[59]; /**< [in] Reserved. Should be set to 0. */
|
||||
void* pReserved[31]; /**< [in] Reserved. Should be set to NULL. */
|
||||
} NvFBCStatusEx;
|
||||
#define NVFBC_STATUS_VER_1 NVFBC_STRUCT_VERSION(NvFBCStatusEx, 1)
|
||||
#define NVFBC_STATUS_VER_2 NVFBC_STRUCT_VERSION(NvFBCStatusEx, 2)
|
||||
#define NVFBC_STATUS_VER NVFBC_STATUS_VER_2
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_STRUCTS
|
||||
* \brief Defines the parameters to be used with NvFBC_CreateEx API.
|
||||
*/
|
||||
typedef struct _NvFBCCreateParams
|
||||
{
|
||||
NvU32 dwVersion; /**< [in] Struct version. Set to NVFBC_CREATE_PARAMS_VER. */
|
||||
NvU32 dwInterfaceType; /**< [in] ID of the NVFBC interface Type being requested. */
|
||||
NvU32 dwMaxDisplayWidth; /**< [out] Max. display width allowed. */
|
||||
NvU32 dwMaxDisplayHeight; /**< [out] Max. display height allowed. */
|
||||
void* pDevice; /**< [in] Device pointer. */
|
||||
void* pPrivateData; /**< [in] Private data [optional]. */
|
||||
NvU32 dwPrivateDataSize; /**< [in] Size of private data. */
|
||||
NvU32 dwInterfaceVersion; /**< [in] Version of the capture interface. */
|
||||
void* pNvFBC; /**< [out] A pointer to the requested NVFBC object. */
|
||||
NvU32 dwAdapterIdx; /**< [in] Adapter Ordinal corresponding to the display to be grabbed. If pDevice is set, this parameter is ignored. */
|
||||
NvU32 dwNvFBCVersion; /**< [out] Indicates the highest NvFBC interface version supported by the loaded NVFBC library. */
|
||||
void* cudaCtx; /**< [in] CUDA context created using cuD3D9CtxCreate with the D3D9 device passed as pDevice. Only used for NvFBCCuda interface.
|
||||
It is mandatory to pass a valid D3D9 device if cudaCtx is passed. The call will fail otherwise.
|
||||
Client must release NvFBCCuda object before destroying the cudaCtx. */
|
||||
void* pPrivateData2; /**< [in] Private data [optional]. */
|
||||
NvU32 dwPrivateData2Size; /**< [in] Size of private data. */
|
||||
NvU32 dwReserved[55]; /**< [in] Reserved. Should be set to 0. */
|
||||
void* pReserved[27]; /**< [in] Reserved. Should be set to NULL. */
|
||||
}NvFBCCreateParams;
|
||||
#define NVFBC_CREATE_PARAMS_VER_1 NVFBC_STRUCT_VERSION(NvFBCCreateParams, 1)
|
||||
#define NVFBC_CREATE_PARAMS_VER_2 NVFBC_STRUCT_VERSION(NvFBCCreateParams, 2)
|
||||
#define NVFBC_CREATE_PARAMS_VER NVFBC_CREATE_PARAMS_VER_2
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_STRUCTS
|
||||
* \brief Defines parameters for a Grab\Capture call to get HW cursor data in the NVFBCToSys capture session.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
NvU32 dwVersion; /**< [in]: Struct version. Set to NVFBC_MOUSE_GRAB_INFO_VER.*/
|
||||
NvU32 dwWidth; /**< [out]: Width of mouse glyph captured.*/
|
||||
NvU32 dwHeight; /**< [out]: Height of mouse glyph captured.*/
|
||||
NvU32 dwPitch; /**< [out]: Pitch of mouse glyph captured.*/
|
||||
NvU32 bIsHwCursor : 1; /**< [out]: Tells if cursor is HW cursor or SW cursor. If set to 0, ignore height, width, pitch and pBits.*/
|
||||
NvU32 bReserved : 32; /**< [in]: Reserved.*/
|
||||
NvU32 dwPointerFlags; /**< [out]: Maps to DXGK_POINTERFLAGS::Value.*/
|
||||
NvU32 dwXHotSpot; /**< [out]: Maps to DXGKARG_SETPOINTERSHAPE::XHot.*/
|
||||
NvU32 dwYHotSpot; /**< [out]: Maps to DXGKARG_SETPOINTERSHAPE::YHot.*/
|
||||
NvU32 dwUpdateCounter; /**< [out]: Cursor update Counter. */
|
||||
NvU32 dwBufferSize; /**< [out]: Size of the buffer contaiing the captured cursor glyph. */
|
||||
void * pBits; /**< [out]: pointer to buffer containing the captured cursor glyph.*/
|
||||
NvU32 dwReservedA[22]; /**< [in]: Reserved. Set to 0.*/
|
||||
void * pReserved[15]; /**< [in]: Reserved. Set to 0.*/
|
||||
}NVFBC_CURSOR_CAPTURE_PARAMS;
|
||||
#define NVFBC_CURSOR_CAPTURE_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_CURSOR_CAPTURE_PARAMS, 1)
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_ENTRYPOINTS
|
||||
* \brief NVFBC API to set global overrides
|
||||
* \param [in] dwFlags Global overrides for NVFBC. Use ::NVFBC_GLOBAL_FLAGS value.
|
||||
*/
|
||||
void NVFBCAPI NvFBC_SetGlobalFlags(DWORD dwFlags);
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_ENTRYPOINTS
|
||||
* \brief NVFBC API to create an NVFBC capture session.
|
||||
* Instantiates an interface identified by NvFBCCreateParams::dwInterfaceType.
|
||||
* \param [inout] pCreateParams Pointer to a struct of type ::NvFBCCreateParams, typecast to void*
|
||||
* \return An applicable ::NVFBCRESULT value.
|
||||
*/
|
||||
NVFBCRESULT NVFBCAPI NvFBC_CreateEx(void * pCreateParams);
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_ENTRYPOINTS
|
||||
* \brief NVFBC API to query Current NVFBC status.
|
||||
* Queries the status for the adapter pointed to by the NvFBCStatusEx::dwAdapterIdx parameter.
|
||||
* \param [inout] pCreateParams Pointer to a struct of type ::NvFBCStatusEx.
|
||||
* \return An applicable ::NVFBCRESULT value.
|
||||
*/
|
||||
NVFBCRESULT NVFBCAPI NvFBC_GetStatusEx(NvFBCStatusEx *pNvFBCStatusEx);
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_ENTRYPOINTS
|
||||
* \brief NVFBC API to enable \ disable NVFBC feature.
|
||||
* \param [in] nvFBCState Refer ::NVFBC_STATE
|
||||
* \return An applicable ::NVFBCRESULT value.
|
||||
*/
|
||||
NVFBCRESULT NVFBCAPI NvFBC_Enable(NVFBC_STATE nvFBCState);
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_ENTRYPOINTS
|
||||
* \brief NVFBC API to query highest GRID SDK version supported by the loaded NVFBC library.
|
||||
* \param [out] pVersion Pointer to a 32-bit integer to hold the supported GRID SDK version.
|
||||
* \return An applicable ::NVFBCRESULT value.
|
||||
*/
|
||||
NVFBCRESULT NVFBCAPI NvFBC_GetSDKVersion(NvU32 * pVersion);
|
||||
|
||||
/**
|
||||
* \cond API_PFN
|
||||
*/
|
||||
typedef void (NVFBCAPI * NvFBC_SetGlobalFlagsType) (DWORD dwFlags);
|
||||
typedef NVFBCRESULT (NVFBCAPI * NvFBC_CreateFunctionExType) (void * pCreateParams);
|
||||
typedef NVFBCRESULT (NVFBCAPI * NvFBC_GetStatusExFunctionType) (void * pNvFBCStatus);
|
||||
typedef NVFBCRESULT (NVFBCAPI * NvFBC_EnableFunctionType) (NVFBC_STATE nvFBCState);
|
||||
typedef NVFBCRESULT (NVFBCAPI * NvFBC_GetSDKVersionFunctionType) (NvU32 * pVersion);
|
||||
/**
|
||||
* \endcond API_PFN
|
||||
*/
|
||||
Vendored
+176
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* \file This file contains defintions for NVFBCToSys
|
||||
*
|
||||
* Copyright 1993-2016 NVIDIA Corporation. All rights reserved.
|
||||
* NOTICE TO LICENSEE: This source code and/or documentation ("Licensed Deliverables")
|
||||
* are subject to the applicable NVIDIA license agreement
|
||||
* that governs the use of the Licensed Deliverables.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef NVFBC_TO_SYS_H_
|
||||
#define NVFBC_TO_SYS_H_
|
||||
/**
|
||||
* \defgroup NVFBC_TOSYS NVFBCToSys Interface
|
||||
* \brief Interface for grabbing Desktop images and generating output in system memory.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \defgroup NVFBC_TOSYS_ENUMS Enums
|
||||
* \ingroup NVFBC_TOSYS
|
||||
* \brief Enumerations used with NVFBCToSys interface.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \defgroup NVFBC_TOSYS_STRUCTS Structs
|
||||
* \ingroup NVFBC_TOSYS
|
||||
* \brief Parameter Structs Defined for use with NVFBCToSys interface.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \defgroup NVFBC_TOSYS_INTERFACE Object Interface
|
||||
* \ingroup NVFBC_TOSYS
|
||||
* \brief Interface class definition for NVFBCToSys Capture API
|
||||
*/
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_TOSYS
|
||||
* \brief Macro to define the interface ID to be passed as NvFBCCreateParams::dwInterfaceType
|
||||
* for creating an NVFBCToSys capture session object.
|
||||
*/
|
||||
#define NVFBC_TO_SYS (0x1204)
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_TOSYS_ENUMS
|
||||
* Enumerates output buffer pixel data formats supported by NVFBCToSys.
|
||||
*/
|
||||
typedef enum
|
||||
{
|
||||
NVFBC_TOSYS_ARGB = 0, /**< Output Pixels in ARGB format: 32bpp, one byte per channel. */
|
||||
NVFBC_TOSYS_RGB , /**< Output Pixels in RGB format: 24bpp, one byte per channel. */
|
||||
NVFBC_TOSYS_YYYYUV420p , /**< Output Pixels in YUV420 format: 12bpp,
|
||||
the Y' channel at full resolution, U channel at half resolution (1 byte for four pixels), V channel at half resolution. */
|
||||
NVFBC_TOSYS_RGB_PLANAR , /**< Output Pixels in planar RGB format: 24bpp,
|
||||
stored sequentially in memory as complete red channel, complete green channel, complete blue channel. */
|
||||
NVFBC_TOSYS_XOR , /**< Output Pixels in RGB format: 24bpp XOR'd with the prior frame. */
|
||||
NVFBC_TOSYS_YUV444p , /**< Output Pixels in YUV444 planar format, i.e. separate 8-bpp Y, U, V planes with no subsampling.*/
|
||||
NVFBC_TOSYS_BUF_FMT_LAST , /**< Sentinel value. Do not use.*/
|
||||
} NVFBCToSysBufferFormat;
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_TOSYS_ENUMS
|
||||
* Enumerates Capture\Grab modes supported by NVFBCToSys.
|
||||
*/
|
||||
typedef enum
|
||||
{
|
||||
NVFBC_TOSYS_SOURCEMODE_FULL = 0, /**< Grab full res */
|
||||
NVFBC_TOSYS_SOURCEMODE_SCALE , /**< Will convert current res to supplied resolution (dwTargetWidth and dwTargetHeight) */
|
||||
NVFBC_TOSYS_SOURCEMODE_CROP , /**< Native res, crops a subwindow, of dwTargetWidth and dwTargetHeight sizes, starting at dwStartX and dwStartY */
|
||||
NVFBC_TOSYS_SOURCEMODE_LAST , /**< Sentinel value. Do not use. */
|
||||
}NVFBCToSysGrabMode;
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_TOSYS_ENUMS
|
||||
* \enum NVFBC_TOSYS_GRAB_FLAGS Enumerates special commands for grab\capture supported by NVFBCToSys.
|
||||
*/
|
||||
typedef enum
|
||||
{
|
||||
NVFBC_TOSYS_NOFLAGS = 0x0, /**< Default (no flags set). Grabbing will wait for a new frame or HW mouse move. */
|
||||
NVFBC_TOSYS_NOWAIT = 0x1, /**< Grabbing will not wait for a new frame nor a HW cursor move. */
|
||||
NVFBC_TOSYS_WAIT_WITH_TIMEOUT = 0x10, /**< Grabbing will wait for a new frame or HW mouse move with a maximum wait time of NVFBC_TOSYS_GRAB_FRAME_PARAMS::dwWaitTime millisecond*/
|
||||
} NVFBC_TOSYS_GRAB_FLAGS;
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_TOSYS_STRUCTS
|
||||
* \brief Defines parameters used to configure NVFBCToSys capture session.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
NvU32 dwVersion; /**< [in]: Struct version. Set to NVFBC_TOSYS_SETUP_PARAMS_VER.*/
|
||||
NvU32 bWithHWCursor :1; /**< [in]: The client should set this to 1 if it requires the HW cursor to be composited on the captured image.*/
|
||||
NvU32 bDiffMap :1; /**< [in]: The client should set this to use the DiffMap feature.*/
|
||||
NvU32 bEnableSeparateCursorCapture : 1; /**< [in]: The client should set this to 1 if it wants to enable mouse capture in separate stream.*/
|
||||
NvU32 bReservedBits :29; /**< [in]: Reserved. Set to 0.*/
|
||||
NVFBCToSysBufferFormat eMode; /**< [in]: Output image format.*/
|
||||
NvU32 dwReserved1; /**< [in]: Reserved. Set to 0.*/
|
||||
void **ppBuffer; /**< [out]: Container to hold NvFBC output buffers.*/
|
||||
void **ppDiffMap; /**< [out]: Container to hold NvFBC output diffmap buffers.*/
|
||||
void *hCursorCaptureEvent; /**< [out]: Client should wait for mouseEventHandle event before calling MouseGrab function. */
|
||||
NvU32 dwReserved[58]; /**< [in]: Reserved. Set to 0.*/
|
||||
void *pReserved[29]; /**< [in]: Reserved. Set to 0.*/
|
||||
} NVFBC_TOSYS_SETUP_PARAMS_V2;
|
||||
#define NVFBC_TOSYS_SETUP_PARAMS_VER2 NVFBC_STRUCT_VERSION(NVFBC_TOSYS_SETUP_PARAMS, 2)
|
||||
typedef NVFBC_TOSYS_SETUP_PARAMS_V2 NVFBC_TOSYS_SETUP_PARAMS;
|
||||
#define NVFBC_TOSYS_SETUP_PARAMS_VER NVFBC_TOSYS_SETUP_PARAMS_VER2
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_TOSYS_STRUCTS
|
||||
* \brief Defines parameters for a Grab\Capture call in the NVFBCToSys capture session.
|
||||
* Also holds information regarding the grabbed data.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
NvU32 dwVersion; /**< [in]: Struct version. Set to NVFBC_TOSYS_GRAB_FRAME_PARAMS_VER.*/
|
||||
NvU32 dwFlags; /**< [in]: Special grabbing requests. This should be a bit-mask of NVFBC_TOSYS_GRAB_FLAGS values.*/
|
||||
NvU32 dwTargetWidth; /**< [in]: Target image width. NvFBC will scale the captured image to fit taret width and height. Used with NVFBC_TOSYS_SOURCEMODE_SCALE and NVFBC_TOSYS_SOURCEMODE_CROP. */
|
||||
NvU32 dwTargetHeight; /**< [in]: Target image height. NvFBC will scale the captured image to fit taret width and height. Used with NVFBC_TOSYS_SOURCEMODE_SCALE and NVFBC_TOSYS_SOURCEMODE_CROP. */
|
||||
NvU32 dwStartX; /**< [in]: x-coordinate of starting pixel for cropping. Used with NVFBC_TOSYS_SOURCEMODE_CROP. */
|
||||
NvU32 dwStartY; /**< [in]: y-coordinate of starting pixel for cropping. Used with NVFBC_TOSYS_SOURCEMODE_CROP. .*/
|
||||
NVFBCToSysGrabMode eGMode; /**< [in]: Frame grab mode.*/
|
||||
NvU32 dwWaitTime; /**< [in]: Time limit for NvFBCToSysGrabFrame() to wait until a new frame is available or a HW mouse moves. Use with NVFBC_TOSYS_WAIT_WITH_TIMEOUT */
|
||||
NvFBCFrameGrabInfo *pNvFBCFrameGrabInfo; /**< [in/out]: Frame grab information and feedback from NvFBC driver.*/
|
||||
NvU32 dwReserved[56]; /**< [in]: Reserved. Set to 0.*/
|
||||
void *pReserved[31]; /**< [in]: Reserved. Set to NULL.*/
|
||||
} NVFBC_TOSYS_GRAB_FRAME_PARAMS_V1;
|
||||
#define NVFBC_TOSYS_GRAB_FRAME_PARAMS_VER1 NVFBC_STRUCT_VERSION(NVFBC_TOSYS_GRAB_FRAME_PARAMS, 1)
|
||||
typedef NVFBC_TOSYS_GRAB_FRAME_PARAMS_V1 NVFBC_TOSYS_GRAB_FRAME_PARAMS;
|
||||
#define NVFBC_TOSYS_GRAB_FRAME_PARAMS_VER NVFBC_TOSYS_GRAB_FRAME_PARAMS_VER1
|
||||
|
||||
|
||||
/**
|
||||
* \ingroup NVFBC_TOSYS_INTERFACE
|
||||
* Interface class definition for NVFBCToSys Capture API
|
||||
*/
|
||||
class INvFBCToSys_v3
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* \brief Sets up NVFBC System Memory capture according to the provided parameters.
|
||||
* \param [in] pParam Pointer to a struct of type ::NVFBC_TOSYS_SETUP_PARAMS.
|
||||
* \return An applicable ::NVFBCRESULT value.
|
||||
*/
|
||||
virtual NVFBCRESULT NVFBCAPI NvFBCToSysSetUp (NVFBC_TOSYS_SETUP_PARAMS_V2 *pParam) = 0;
|
||||
|
||||
/**
|
||||
* \brief Captures the desktop and dumps the captured data to a System memory buffer.
|
||||
* If the API returns a failure, the client should check the return codes and ::NvFBCFrameGrabInfo output fields to determine if the session needs to be re-created.
|
||||
* \param [inout] pParam Pointer to a struct of type ::NVFBC_TOSYS_GRAB_FRAME_PARAMS.
|
||||
* \return An applicable ::NVFBCRESULT value.
|
||||
*/
|
||||
virtual NVFBCRESULT NVFBCAPI NvFBCToSysGrabFrame (NVFBC_TOSYS_GRAB_FRAME_PARAMS *pParam) = 0;
|
||||
|
||||
/**
|
||||
* \brief Captures HW cursor data whenever shape of mouse is changed
|
||||
* \param [inout] pParam Pointer to a struct of type ::NVFBC_CURSOR_CAPTURE_PARAMS.
|
||||
* \return An applicable ::NVFBCRESULT value.
|
||||
*/
|
||||
virtual NVFBCRESULT NVFBCAPI NvFBCToSysCursorCapture (NVFBC_CURSOR_CAPTURE_PARAMS *pParam) = 0;
|
||||
|
||||
/**
|
||||
* \brief A high precision implementation of Sleep().
|
||||
* Can provide sub quantum (usually 16ms) sleep that does not burn CPU cycles.
|
||||
* \param [in] qwMicroSeconds The number of microseconds that the thread should sleep for.
|
||||
* \return An applicable ::NVFBCRESULT value.
|
||||
*/
|
||||
virtual NVFBCRESULT NVFBCAPI NvFBCToSysGPUBasedCPUSleep (__int64 qwMicroSeconds) = 0;
|
||||
|
||||
/**
|
||||
* \brief Destroys the NVFBCToSys capture session.
|
||||
* \return An applicable ::NVFBCRESULT value.
|
||||
*/
|
||||
virtual NVFBCRESULT NVFBCAPI NvFBCToSysRelease () = 0;
|
||||
};
|
||||
|
||||
typedef INvFBCToSys_v3 NvFBCToSys;
|
||||
|
||||
#endif // NVFBC_TO_SYS_H_
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
/* Windows.h — case-compat shim for the vendor NvFBC header, not our API. */
|
||||
#include <windows.h>
|
||||
Reference in New Issue
Block a user