Compare commits

..

No commits in common. "96329923f62c744419fe3f30521a3997932fd714" and "af21cd3686c70fcdb36d4be17620d18d931ce2f6" have entirely different histories.

21 changed files with 174 additions and 264 deletions

3
.gitignore vendored
View file

@ -1,5 +1,4 @@
/client/target
/testclient/target
/target
# agent
/agent/.cache

View file

@ -2,12 +2,6 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
[[package]]
name = "autocfg"
version = "1.4.0"
@ -47,12 +41,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "dlib"
version = "0.5.2"
@ -84,6 +72,14 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
[[package]]
name = "fbcserver"
version = "0.1.0"
dependencies = [
"cc",
"minifb",
]
[[package]]
name = "futures"
version = "0.3.31"
@ -173,24 +169,6 @@ dependencies = [
"slab",
]
[[package]]
name = "hazelnut_client"
version = "0.1.0"
dependencies = [
"anyhow",
"cc",
"nix 0.29.0",
]
[[package]]
name = "hazelnut_testclient"
version = "0.1.0"
dependencies = [
"anyhow",
"hazelnut_client",
"minifb",
]
[[package]]
name = "instant"
version = "0.1.13"
@ -310,18 +288,6 @@ dependencies = [
"memoffset",
]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "once_cell"
version = "1.20.2"
@ -586,7 +552,7 @@ dependencies = [
"bitflags 1.3.2",
"downcast-rs",
"libc",
"nix 0.24.3",
"nix",
"scoped-tls",
"wayland-commons",
"wayland-scanner",
@ -599,7 +565,7 @@ version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902"
dependencies = [
"nix 0.24.3",
"nix",
"once_cell",
"smallvec",
"wayland-sys",
@ -611,7 +577,7 @@ version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661"
dependencies = [
"nix 0.24.3",
"nix",
"wayland-client",
"xcursor",
]

10
Cargo.toml Normal file
View file

@ -0,0 +1,10 @@
[package]
name = "fbcserver"
version = "0.1.0"
edition = "2021"
[dependencies]
minifb = "0.27.0"
[build-dependencies]
cc = "1.0.99"

View file

@ -6,13 +6,10 @@ Currently NVIDIA-specific (since we use NvFBC); support for DXGI duplication (fo
The agent runs on Windows 7 x64 onwards. Lower (or x86) is currently not supported as a target.
The agent uses IVSHMEM to provide frames to the client in a low-latency fashion.
Later on virtio-serial may be considered for low-bandwidth additional functionality, like input. For now, however, the agent is display only.
The agent uses IVSHMEM to provide frames to the client in a low-latency fashion. Later on virtio-serial may be considered for low-bandwidth additional functionality, like input. For now, however, the agent is display only.
This repository contains:
- The agent. Written in C++20.
- Shared headers and utility code, used between the agent and the client. Written in C++20, mostly so it can be shared between the agent and client.
- A Rust crate for Hazelnut clients to use. Wraps the Shared code.
- A test client written in Rust which displays the framebuffer (in a quick-and-dirty fashion.).
- A simple client, written mostly in Rust (with some C++ glue), which displays the framebuffer (in a quick-and-dirty fashion.). Mostly test-only code. Some of it isn't though!

View file

@ -6,13 +6,13 @@ namespace hazelnut {
enum class DisplayCaptureResult {
Ok,
OkUnchanged, // OK but nothing changed
OkButResized, // OK, but grab the buffer information again. It's different due to a resize
Fail, // epic fail
};
// Framebuffer information
struct FramebufferInformation {
u32* pFramebuffer;
u32 width;
u32 height;
};
@ -30,8 +30,12 @@ namespace hazelnut {
virtual bool Initialize() = 0;
/// Captures frame to provided output buffer directly.
virtual DisplayCaptureResult CaptureFrame(u32* pOut) = 0;
/// New CaptureFrame() API; now capture is copied directly to output buffer
/// instead of copy->copy.
virtual DisplayCaptureResult CaptureFrameTemp(u32* pOut) = 0;
/// Performs the capture. THIS VERSION IS DEPRECATED AND WILL BE REMOVED
virtual DisplayCaptureResult CaptureFrame() = 0;
/// Get framebuffer information.
virtual FramebufferInformation GetFramebufferInformation() = 0;

View file

@ -68,6 +68,8 @@ namespace hazelnut {
u32 height;
u32* pRawFramebuffer;
unique_buffer<u32> convertedFramebuffer;
u8* pDiffMap;
u32 diffmapWidth;
@ -167,7 +169,7 @@ namespace hazelnut {
return true;
}
DisplayCaptureResult PaintWithTileOptimization(u32* pBufferData) {
void PaintWithTileOptimization(u32* pBufferData) {
bool copiedTiles = false;
for(u32 dy = 0; dy < diffmapHeight; ++dy) {
@ -196,11 +198,6 @@ namespace hazelnut {
}
}
}
if(!copiedTiles)
return DisplayCaptureResult::OkUnchanged;
return DisplayCaptureResult::Ok;
}
void PaintFull(u32* pBufferData) {
@ -222,7 +219,7 @@ namespace hazelnut {
}
}
DisplayCaptureResult CaptureFrame(u32* pOut) override {
DisplayCaptureResult CaptureFrameTemp(u32* pOut) override {
auto nvStatus = nvfbc->NvFBCToSysGrabFrame(&this->fbcSysGrabParams);
switch(nvStatus) {
@ -235,7 +232,7 @@ namespace hazelnut {
// Recurse. This looks naughty, but will allow us to directly retry
// the capture. (Plus if this causes issues whatever has happened is probably beyond saving)
return CaptureFrame(pOut);
return CaptureFrame();
} break;
default: return DisplayCaptureResult::Fail;
@ -262,7 +259,7 @@ namespace hazelnut {
}
if(useTilePainting) [[likely]] {
result = PaintWithTileOptimization(&pOut[0]);
PaintWithTileOptimization(&pOut[0]);
} else {
PaintFull(&pOut[0]);
}
@ -270,7 +267,62 @@ namespace hazelnut {
return result;
}
FramebufferInformation GetFramebufferInformation() override { return FramebufferInformation { width, height }; }
DisplayCaptureResult CaptureFrame() override {
auto nvStatus = nvfbc->NvFBCToSysGrabFrame(&this->fbcSysGrabParams);
switch(nvStatus) {
case NVFBC_SUCCESS: break;
// Need to recreate the session. If it fails then we fail too.
case NVFBC_ERROR_INVALIDATED_SESSION: {
if(!NvfbcInitSession())
return DisplayCaptureResult::Fail;
// Recurse. This looks naughty, but will allow us to directly retry
// the capture. (Plus if this causes issues whatever has happened is probably beyond saving)
return CaptureFrame();
} break;
default: return DisplayCaptureResult::Fail;
}
if(width != grabInfo.dwWidth || height != grabInfo.dwHeight) {
width = grabInfo.dwWidth;
height = grabInfo.dwHeight;
convertedFramebuffer.resize(width * height);
return DisplayCaptureResult::OkButResized;
}
// Splat to the converted framebuffer. It doesn't have padding on it.
auto* pBufferData = (u8*)convertedFramebuffer.data();
auto* pSrcData = (u8*)&pRawFramebuffer[0];
for(u32 y = 0; y < grabInfo.dwHeight; ++y) {
// Convert to BGRA
// FIXME: Make this SIMD. I can't into this very well
#if 0
usize srcStart = (y * grabInfo.dwBufferWidth) * 4;
usize dstStart = (y * width) * 4;
for(u32 x = 0; x < grabInfo.dwWidth * 4; x += 4) {
pBufferData[(dstStart + x) + 0] = pSrcData[(srcStart + x) + 2]; // B
pBufferData[(dstStart + x) + 1] = pSrcData[(srcStart + x) + 1]; // G
pBufferData[(dstStart + x) + 2] = pSrcData[(srcStart + x) + 0]; // R
pBufferData[(dstStart + x) + 3] = 0xff; // A
}
#endif
memcpy(&pBufferData[(y * width) * 4], &pRawFramebuffer[(y * grabInfo.dwBufferWidth)], grabInfo.dwWidth * 4);
}
// sleep
// nvfbc->NvFBCToSysGPUBasedCPUSleep(16000);
return DisplayCaptureResult::Ok;
}
FramebufferInformation GetFramebufferInformation() override { return FramebufferInformation { convertedFramebuffer.data(), width, height }; }
DiffInformation GetDiffInformation() override {
// diffmapWidth = (u32)ceil((f32)width / 32);

View file

@ -51,28 +51,22 @@ int main(int argc, char** argv) {
while(true) {
{
auto guard = pHeader->lock.lock();
auto result = pCaptureInterface->CaptureFrame(pFrameHeader->bits());
auto result = pCaptureInterface->CaptureFrameTemp(pFrameHeader->bits());
switch(result) {
case hazelnut::DisplayCaptureResult::Ok:
case hazelnut::DisplayCaptureResult::OkButResized:
if(result == hazelnut::DisplayCaptureResult::OkButResized) {
framebuffer = pCaptureInterface->GetFramebufferInformation();
diff = pCaptureInterface->GetDiffInformation();
}
pFrameHeader->serial.fetch_add(1);
pFrameHeader->width.store(framebuffer.width);
pFrameHeader->height.store(framebuffer.height);
break;
case hazelnut::DisplayCaptureResult::OkUnchanged: continue; break;
default:
printf("Failed to capture");
return 1;
break;
if(result == hazelnut::DisplayCaptureResult::Ok) {
// Do nothing.
} else if(result == hazelnut::DisplayCaptureResult::OkButResized) {
// We resized. Notify of that
framebuffer = pCaptureInterface->GetFramebufferInformation();
diff = pCaptureInterface->GetDiffInformation();
} else {
printf("Failed to capture\n");
break;
}
pFrameHeader->serial.fetch_add(1);
pFrameHeader->width.store(framebuffer.width);
pFrameHeader->height.store(framebuffer.height);
}
}

14
build.rs Normal file
View file

@ -0,0 +1,14 @@
use cc;
fn main() {
let mut build = cc::Build::new();
build
.emit_rerun_if_env_changed(true)
.cpp(true)
.std("c++20")
.include("shared/src")
.file("shared/src/ivshmem.cpp")
.file("src/rust_wrapper.cpp")
.compile("rust_ivshmem_bare");
}

69
client/Cargo.lock generated
View file

@ -1,69 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "cc"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "hazelnut_client"
version = "0.1.0"
dependencies = [
"anyhow",
"cc",
"nix",
]
[[package]]
name = "libc"
version = "0.2.167"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc"
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"

View file

@ -1,12 +0,0 @@
[package]
name = "hazelnut_client"
description = "Low-level client for the Hazelnut IVSHMEM display protocol"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.93"
nix = { version = "0.29.0", features = [ "fs" ] }
[build-dependencies]
cc = "1.0.99"

View file

@ -1,18 +0,0 @@
use cc;
fn main() {
let mut build = cc::Build::new();
// HACK: cc sucks
println!("cargo:rerun-if-changed=src/rust_wrapper.cpp");
println!("cargo:rerun-if-changed=../shared/src");
build
.emit_rerun_if_env_changed(true)
.cpp(true)
.std("c++20")
.include("../shared/src")
.file("../shared/src/ivshmem.cpp")
.file("src/rust_wrapper.cpp")
.compile("hazelnut_client_cpp_native");
}

View file

@ -1,2 +0,0 @@
pub mod client;
pub use client::*;

View file

@ -21,8 +21,8 @@ namespace hazelnut {
delete pImpl;
}
bool IvshmemDevice::Open(int fd) {
return pImpl->Open(fd);
bool IvshmemDevice::Open(const char* devName) {
return pImpl->Open(devName);
}
void IvshmemDevice::Close() {

View file

@ -14,8 +14,8 @@ namespace hazelnut {
~IvshmemDevice();
/// Opens the device. On Linux [fd] MUST be a valid file descriptor for a SHMEM file.
bool Open(int fd = -1);
/// Opens the device. On Linux [devName] MUST point to a SHMEM file.
bool Open(const char* devName = nullptr);
void Close();

View file

@ -15,12 +15,17 @@ namespace hazelnut {
void* pMap = nullptr;
usize size = 0;
bool OpenDevice(int fd) {
// fd is not optional on Linux.
if(fd == -1)
bool OpenDevice(const char* devName) {
// devName is not optional on Linux.
if(devName == nullptr)
return false;
this->fd = fd;
int devFd = open(devName, O_RDWR, static_cast<mode_t>(0600));
if(devFd == -1)
return false;
fd = devFd;
return true;
}
@ -36,6 +41,7 @@ namespace hazelnut {
void* map = mmap(0, devSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(map == MAP_FAILED) {
printf("map failed\n");
return false;
}
@ -50,8 +56,8 @@ namespace hazelnut {
munmap(pMap, size);
}
bool Open(int fd) {
if(!OpenDevice(fd))
bool Open(const char* pDevName) {
if(!OpenDevice(pDevName))
return false;
if(!MapMemory()) {

View file

@ -138,7 +138,7 @@ namespace hazelnut {
memset(&ivshmemMmapStruct, 0, sizeof(IVSHMEM_MMAP));
}
bool Open(int fd) {
bool Open(const char* pDevName) {
if(!OpenDevice())
return false;

View file

@ -1,8 +1,4 @@
#include <fcntl.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <thread>
#include "atomic_spinlock.hpp"
@ -15,13 +11,7 @@ int main(int argc, char** argv) {
hazelnut::IvshmemDevice dev;
int fd = open("/dev/shm/lg-win7", O_RDWR, (mode_t)0600);
if(fd == -1) {
printf("Failed to open ivshmem device file\n");
}
if(!dev.Open(fd)) {
if(!dev.Open("/dev/shm/lg-win7")) {
printf("Failed to open ivshmem device\n");
return 1;
}
@ -31,11 +21,6 @@ int main(int argc, char** argv) {
printf("opened a %zu MB large ivshmem\n", (size / (1024 * 1024)));
memset(ptr, 0, size);
printf("zeroed :)\n");
#if 0
auto* pHeader = (hazelnut::IvshHeader*)&ptr[0];;
auto* pFrameHeader = (hazelnut::FrameHeader*)&ptr[0x1000];
@ -74,6 +59,6 @@ int main(int argc, char** argv) {
// allow the vm some time to do whatever it is it wants to do
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
#endif
return 0;
}

View file

@ -1,5 +1,5 @@
//! Hazelnut C++ client bindings.
use nix::{fcntl::OFlag, sys::stat::Mode, NixPath};
use std::ffi;
pub(crate) mod sys {
use std::ffi;
@ -13,9 +13,6 @@ pub(crate) mod sys {
/// The frame has been changed.
Changed,
/// The Hazelnut lock was contended, so we couldn't lock it ourselves.
LockContended,
/// A failure occured during the tick.
Fail,
}
@ -24,7 +21,10 @@ pub(crate) mod sys {
pub(crate) fn rust_new_hazelnut_client() -> *mut ffi::c_void;
pub(crate) fn rust_destroy_hazelnut_client(client: *mut ffi::c_void);
pub(crate) fn rust_hazelnut_client_open(client: *mut ffi::c_void, fd: ffi::c_int) -> bool;
pub(crate) fn rust_hazelnut_client_open(
client: *mut ffi::c_void,
url: *const ffi::c_char,
) -> bool;
pub(crate) fn rust_hazelnut_client_tick(client: *mut ffi::c_void) -> ResultCode;
@ -62,19 +62,14 @@ impl HazelnutClient {
}
/// Opens the given IVSHMEM shmem file.
pub fn open<P: ?Sized + NixPath>(&mut self, path: &P) -> anyhow::Result<()> {
// NOTE: `fd` is owned by the hazelnut client once it is provided to it, so we do not close it ourselves.
let fd = nix::fcntl::open(path, OFlag::O_RDWR, Mode::S_IRUSR | Mode::S_IWUSR)?;
pub fn open(&mut self, path: &String) -> bool {
let cstr = ffi::CString::new(path.clone()).expect("dumbass");
// FIXME: this really should work by FD so we can just return io::Result<> or something, but bleh
// for now it's "fine", also it's a path in the shared sources currently.
unsafe {
if !sys::rust_hazelnut_client_open(self.0, fd) {
return Err(anyhow::anyhow!("Failed to open Hazelnut client"));
}
return sys::rust_hazelnut_client_open(self.0, cstr.as_ptr());
}
Ok(())
}
/// Ticks this client.

View file

@ -1,19 +1,22 @@
use minifb::{Window, WindowOptions};
use hazelnut_client::*;
mod hzclient;
fn main() -> anyhow::Result<()> {
fn main() {
let mut screen_width: u32 = 320;
let mut screen_height: u32 = 200;
let mut window: Option<Window> = None;
let mut client = HazelnutClient::new();
let mut client = hzclient::HazelnutClient::new();
let socket_path: String = "/dev/shm/lg-win7".into();
client.open(&socket_path[..])?;
println!("Opened IVSHMEM device.");
if !client.open(&socket_path) {
println!("Could not open IVSHMEM.");
return ();
} else {
println!("Opened IVSHMEM device.");
}
loop {
match window.as_mut() {
@ -26,8 +29,8 @@ fn main() -> anyhow::Result<()> {
}
match client.tick_one() {
ResultCode::Fail => break,
ResultCode::Changed => {
hzclient::ResultCode::Fail => break,
hzclient::ResultCode::Changed => {
let dims = client.dimensions();
if screen_width != dims.0 && screen_height != dims.1 {
@ -56,22 +59,13 @@ fn main() -> anyhow::Result<()> {
.expect("Failed to update");
client.unlock();
},
ResultCode::LockContended => {
// contended
}
ResultCode::Unchanged => match window.as_mut() {
hzclient::ResultCode::Unchanged => match window.as_mut() {
Some(window) => {
client.unlock();
window.update();
}
_ => {}
},
}
}
Ok(())
}

View file

@ -2,11 +2,11 @@
#include "ivshmem_protocol.hpp"
#include "Utils.hpp"
enum class ResultCode : u32 { Unchanged, Changed, LockContended, Fail };
enum class ResultCode : u32 { Unchanged, Changed, Fail };
struct HazelnutIvshmemClient {
bool Open(int fd) {
if(!ivshmemDevice.Open(fd))
bool Open(const char* path) {
if(!ivshmemDevice.Open(path))
return false;
auto* ptr = (u8*)ivshmemDevice.GetPointer();
@ -14,30 +14,34 @@ struct HazelnutIvshmemClient {
pHeader = (hazelnut::IvshHeader*)&ptr[0];
pFrameHeader = (hazelnut::FrameHeader*)&ptr[0x1000];
sessionId = pHeader->serverSessionId.load();
serial = pHeader->serverSessionId.load();
lastFrameSerial = 0;
return true;
}
// NOTE: This function leaves the lock held on successful returns.
// THIS LEAVES THE LOCK HELD SO YOU CAN READ ON CHANGE!!!!
ResultCode TickOne() {
if(!pHeader)
return ResultCode::Fail;
if(sessionId != pHeader->serverSessionId.load())
if(serial != pHeader->serverSessionId.load())
return ResultCode::Fail;
if(pHeader->lock.try_lock_manually()) {
// failed to lock
return ResultCode::LockContended;
return ResultCode::Unchanged;
}
auto current = pFrameHeader->serial.load();
if(current == lastFrameSerial) {
pHeader->lock.unlock();
return ResultCode::Unchanged;
}
lastFrameSerial = current;
// printf("Frame with serial %u. Width %ux%u\n", lastSerial, pFrameHeader->width.load(), pFrameHeader->height.load());
// pHeader->lock.unlock();
return ResultCode::Changed;
}
@ -59,7 +63,7 @@ struct HazelnutIvshmemClient {
hazelnut::IvshHeader* pHeader;
hazelnut::FrameHeader* pFrameHeader;
u32 sessionId {};
u32 serial {};
u32 lastFrameSerial {};
};
@ -76,9 +80,9 @@ void rust_destroy_hazelnut_client(void* pClient) {
delete(HazelnutIvshmemClient*)pClient;
}
bool rust_hazelnut_client_open(void* pClient, int fd) {
bool rust_hazelnut_client_open(void* pClient, const char* pszPath) {
if(pClient) {
return ((HazelnutIvshmemClient*)pClient)->Open(fd);
return ((HazelnutIvshmemClient*)pClient)->Open(pszPath);
}
return false;

View file

@ -1,9 +0,0 @@
[package]
name = "hazelnut_testclient"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.93"
minifb = "0.27.0"
hazelnut_client = { path = "../client" }