working ivshmem setup

This commit is contained in:
Lily Tsuru 2024-12-01 22:08:11 -05:00
parent 7a6ef52144
commit 758df315a1
13 changed files with 387 additions and 279 deletions

34
Cargo.lock generated
View file

@ -26,12 +26,6 @@ version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.2.1"
@ -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"
@ -88,10 +76,8 @@ checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
name = "fbcserver"
version = "0.1.0"
dependencies = [
"byteorder",
"libc",
"cc",
"minifb",
"nix 0.29.0",
]
[[package]]
@ -302,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"
@ -578,7 +552,7 @@ dependencies = [
"bitflags 1.3.2",
"downcast-rs",
"libc",
"nix 0.24.3",
"nix",
"scoped-tls",
"wayland-commons",
"wayland-scanner",
@ -591,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",
@ -603,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",
]

View file

@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
byteorder = "1.5.0"
libc = "0.2.167"
minifb = "0.27.0"
nix = { version = "0.29.0", features = [ "stat", "mman" ] }
[build-dependencies]
cc = "1.0.99"

View file

@ -33,7 +33,6 @@ LINK_LIBS := $(VS2022_PATH)/ucrt/lib/$(ARCH)/libucrt$(D).lib \
$(VS2022_PATH)/winsdk/lib/$(ARCH)/user32.lib \
$(VS2022_PATH)/winsdk/lib/$(ARCH)/comctl32.lib \
$(VS2022_PATH)/winsdk/lib/$(ARCH)/setupapi.lib \
$(VS2022_PATH)/winsdk/lib/$(ARCH)/ws2_32.lib
.PHONY: all dumpinfo clean matrix

View file

@ -133,7 +133,7 @@ namespace hazelnut {
blockSize = NVFBC_TOSYS_DIFFMAP_BLOCKSIZE_32X32;
// set up a session
NVFBC_TOSYS_SETUP_PARAMS fbcSysSetupParams{};
NVFBC_TOSYS_SETUP_PARAMS fbcSysSetupParams {};
fbcSysSetupParams.dwVersion = NVFBC_TOSYS_SETUP_PARAMS_VER;
fbcSysSetupParams.eMode = NVFBC_TOSYS_ARGB;
fbcSysSetupParams.bWithHWCursor = true;
@ -197,8 +197,6 @@ namespace hazelnut {
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
@ -224,8 +222,8 @@ namespace hazelnut {
FramebufferInformation GetFramebufferInformation() override { return FramebufferInformation { convertedFramebuffer.data(), width, height }; }
DiffInformation GetDiffInformation() override {
//diffmapWidth = (u32)ceil((f32)width / 32);
//diffmapHeight = (u32)ceil((f32)height / 32);
// diffmapWidth = (u32)ceil((f32)width / 32);
// diffmapHeight = (u32)ceil((f32)height / 32);
diffmapWidth = DiffMapDimension(width, blockSize);
diffmapHeight = DiffMapDimension(height, blockSize);

View file

@ -12,17 +12,17 @@
#include "Utils.hpp"
// clang-format on
#include "atomic_spinlock.hpp"
#include "ivshmem_protocol.hpp"
struct tileRect {
u32 x, y, width, height;
};
struct Test {
hazelnut::AtomicSpinlock lk{};
hazelnut::AtomicSpinlock lk {};
std::atomic<u32> sessionId {};
std::atomic<u32> pingPong{};
std::atomic<u32> pingPong {};
};
int main(int argc, char** argv) {
@ -34,43 +34,108 @@ int main(int argc, char** argv) {
}
auto size = dev.GetSize();
auto ptr = (u32*)dev.GetPointer();
auto ptr = (u8*)dev.GetPointer();
printf("opened a %zu MB large ivshmem\n", (size / (1024 * 1024)));
// wipe the first 1mb
memset(&ptr[0], 0, 1*(1024*1024));
memset(&ptr[0], 0, 1 * (1024 * 1024));
printf("wiped memory\n");
// Setup a test struct at the start of ivshmem
auto* pTest = new(&ptr[0]) Test;
// sex
auto* pHeader = new(&ptr[0]) hazelnut::IvshHeader {};
auto* pFrameHeader = new(&ptr[0x1000]) hazelnut::FrameHeader {};
// reset pingpong counter
pTest->pingPong.store(0);
u32 tries = 0;
u32 curTries = 10;
pHeader->serverSessionId.store(rand());
pTest->sessionId.store(rand());
// Create a capture interface
auto capture = hazelnut::CreateDisplayCapture(hazelnut::GuessBestCaptureInterface());
if(!capture) {
printf("Failed to create a capture interface\n");
return 1;
}
printf("Successfully created a framebuffer capture interface\n");
bool firstFrame = true;
std::vector<tileRect> tiles {};
hazelnut::FramebufferInformation framebuffer {};
hazelnut::DiffInformation diff {};
while(true) {
auto result = capture->CaptureFrame();
if(result == hazelnut::DisplayCaptureResult::Ok) {
tiles.clear();
#if 0
if(firstFrame == false) {
for(u32 y = 0; y < diff.diffMapHeight; ++y) {
for(u32 x = 0; x < diff.diffmapWidth; ++x) {
auto& bl = diff.pDiffMap[y * diff.diffmapWidth + x];
if(bl != 0) {
tiles.push_back(tileRect {
x * (framebuffer.width / diff.diffmapWidth), // x
y * (framebuffer.height / diff.diffMapHeight), // y
framebuffer.width / diff.diffmapWidth, // width
framebuffer.height / diff.diffMapHeight // height
});
}
}
}
if(tiles.empty())
continue;
}
#endif
{
auto guard = pHeader->lock.lock();
pFrameHeader->serial.fetch_add(1);
pFrameHeader->width.store(framebuffer.width);
pFrameHeader->height.store(framebuffer.height);
if(framebuffer.pFramebuffer == nullptr)
continue;
memcpy(pFrameHeader->bits(), &framebuffer.pFramebuffer[0], (framebuffer.width * framebuffer.height) * 4);
//printf("FRAME SERIAL %u loaded\n", pFrameHeader->serial.load());
}
if(firstFrame)
firstFrame = false;
} else if(result == hazelnut::DisplayCaptureResult::OkButResized) {
// We resized. Notify of that
framebuffer = capture->GetFramebufferInformation();
diff = capture->GetDiffInformation();
firstFrame = true;
} else {
printf("Failed to capture\n");
break;
}
}
#if 0
while(true) {
// lock
{
auto guard = pTest->lk.lock();
auto guard = pHeader->lk.lock();
Sleep(5);
}
printf("pingpong %u\n", pTest->pingPong.load());
printf("pingpong %u\n", pHeader->pingPong.load());
if(tries++ == curTries) {
tries = 0;
pTest->pingPong.fetch_add(1);
pHeader->pingPong.fetch_add(1);
}
}
#endif
return 0;

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");
}

View file

@ -4,33 +4,35 @@
namespace hazelnut {
#pragma pack(push, 4)
#pragma pack(push, 1)
/// Header for Hazelnut ivshmem. At 0x0 in the ivshmem memory space.
struct alignas(4096) IvshHeader {
struct IvshHeader {
std::atomic<u32> magic;
std::atomic<u32> serverSessionId{};
// When this lock is held by the host, the host can read the rest of memory freely.
// We wait for the host to release before updating memory ourselves.
AtomicSpinlock lock;
// See the next page boundary for FrameHeader
// Immediately after is FrameHeader
};
/// Stored at a page boundary
struct alignas(4096) FrameHeader {
struct FrameHeader {
/// The serial of the frame. If this is unchanged between a
/// lock-grab-unlock cycle, the frame has not changed.
std::atomic<u32> serial;
std::atomic<u32> serial{};
// size
std::atomic<u32> width;
std::atomic<u32> height;
std::atomic<u32> width{};
std::atomic<u32> height{};
// obtain bits at the next page!
// obtain bits at the next page boundary
u32* bits() {
return reinterpret_cast<u32*>(this + 1);
return reinterpret_cast<u32*>(reinterpret_cast<u8*>(this) + 0x1000);
}
};

View file

@ -4,12 +4,10 @@
#include "atomic_spinlock.hpp"
#include "ivshmem.hpp"
#include "ivshmem_protocol.hpp"
int main(int argc, char** argv) {
struct Test {
hazelnut::AtomicSpinlock lk {};
std::atomic<u32> sessionId {};
std::atomic<u32> pingPong {};
};
hazelnut::IvshmemDevice dev;
@ -19,55 +17,43 @@ int main(int argc, char** argv) {
}
auto size = dev.GetSize();
auto ptr = (u32*)dev.GetPointer();
auto ptr = (u8*)dev.GetPointer();
printf("opened a %zu MB large ivshmem\n", (size / (1024 * 1024)));
auto* pTest = (Test*)ptr;
u32 last = 0;
u32 nrSame = 0;
auto* pHeader = (hazelnut::IvshHeader*)&ptr[0];;
auto* pFrameHeader = (hazelnut::FrameHeader*)&ptr[0x1000];
u64 initalizedSessionId = pTest->sessionId.load();
u64 initalizedSessionId = pHeader->serverSessionId.load();
u32 lastSerial = 0;
while(true) {
//printf("%d\n", pTest->timestamp.load());
if(initalizedSessionId != pTest->sessionId.load()) {
if(initalizedSessionId != pHeader->serverSessionId.load()) {
printf("Agent restarted. Closing\n");
break;
}
// need monotonic way
#if 0
if(auto curTimestamp = pTest->timestamp.load(); curTimestamp < lastTimestamp) {
//printf("Not connected or agent crashed %d, %d\n", pTest->timestamp.load() , time(nullptr));
printf("Not connected or agent crashed. %zu\n", curTimestamp);
break;
} else {
lastTimestamp = curTimestamp;
}
#endif
// lock for a bit
{
if(pTest->lk.try_lock_manually()) {
if(pHeader->lock.try_lock_manually()) {
// failed to lock
continue;
}
auto current = pTest->pingPong.load();
auto current = pFrameHeader->serial.load();
if(current == last) {
pTest->lk.unlock();
if(current == lastSerial) {
pHeader->lock.unlock();
continue;
}
nrSame = 0;
last = current;
printf("pingpong %u\n", current);
lastSerial = current;
printf("Frame with serial %u. Width %ux%u\n", lastSerial, pFrameHeader->width.load(), pFrameHeader->height.load());
pTest->lk.unlock();
pHeader->lock.unlock();
}
// allow the vm some time to do whatever it is it wants to do

View file

@ -1,37 +0,0 @@
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
#[repr(C, packed(4))]
pub struct AtomicSpinlock {
lock: AtomicU32
}
pub struct AtomicSpinlockGuard<'a> {
lock: &'a mut AtomicSpinlock,
}
impl Drop for AtomicSpinlockGuard {
fn drop(&mut self) {
self.lock.store(0, Ordering::SeqCst);
}
}
impl AtomicSpinlock {
fn init(&mut self) {
self.lock.store(0, Ordering::SeqCst);
}
fn lock(&mut self) -> AtomicSpinlockGuard<'_> {
loop {
match self
.sync
.compare_exchange(cur, 1, Ordering::SeqCst, Ordering::SeqCst)
{
Ok(last) => return HzLockGuard { lock: self },
Err(_) => {}
}
}
}
}

89
src/hzclient.rs Normal file
View file

@ -0,0 +1,89 @@
use std::{ffi, path::Path};
#[repr(u32)]
pub enum ResultCode {
Unchanged,
Changed,
Fail,
}
extern "C" {
fn rust_new_hazelnut_client() -> *mut ffi::c_void;
fn rust_destroy_hazelnut_client(client: *mut ffi::c_void);
fn rust_hazelnut_client_open(client: *mut ffi::c_void, url: *const ffi::c_char) -> bool;
fn rust_hazelnut_client_tick(client: *mut ffi::c_void) -> ResultCode;
fn rust_hazelnut_client_lock(client: *mut ffi::c_void);
fn rust_hazelnut_client_unlock(client: *mut ffi::c_void);
fn rust_hazelnut_client_get_framebuffer(client: *mut ffi::c_void) -> *mut u32;
fn rust_hazelnut_client_get_width(client: *mut ffi::c_void) -> u32;
fn rust_hazelnut_client_get_height(client: *mut ffi::c_void) -> u32;
}
pub struct HazelnutClient(*mut std::ffi::c_void);
impl HazelnutClient {
pub fn new() -> HazelnutClient {
unsafe { Self(rust_new_hazelnut_client()) }
}
pub fn open(&mut self, path: String) -> bool {
let cstr = ffi::CString::new(path).expect("dumbass");
unsafe {
return rust_hazelnut_client_open(self.0, cstr.as_ptr());
}
}
pub fn tick_one(&mut self) -> ResultCode {
unsafe {
return rust_hazelnut_client_tick(self.0);
}
}
pub fn lock(&mut self) {
unsafe {
return rust_hazelnut_client_lock(self.0);
}
}
pub fn unlock(&mut self) {
unsafe {
return rust_hazelnut_client_unlock(self.0);
}
}
/// only use while lock is held!
pub fn dimensions(&mut self) -> (u32, u32) {
let tup = unsafe {
let width = rust_hazelnut_client_get_width(self.0);
let height = rust_hazelnut_client_get_height(self.0);
(width, height)
};
tup
}
/// only use while lock is held!
pub fn framebuffer(&mut self) -> &mut [u32] {
let sl = unsafe {
let fb_ptr = rust_hazelnut_client_get_framebuffer(self.0);
let dim = self.dimensions();
std::slice::from_raw_parts_mut(fb_ptr, ((dim.0 * dim.1) as usize * 4))
};
sl
}
}
impl Drop for HazelnutClient {
fn drop(&mut self) {
unsafe {
rust_destroy_hazelnut_client(self.0);
}
self.0 = std::ptr::null_mut();
}
}

View file

@ -1,30 +0,0 @@
//! Utilities for ivshmem
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use crate::atomic_spinlock::AtomicSpinlock;
// at 0x0 in ivshmem map
#[repr(C, align(4096))]
pub struct HzHeader {
magic: AtomicU32,
sync: AtomicSpinlock,
// frame serial. if this is unchanged from prev. load it's not the same
frame_serial: AtomicU32,
}
impl HzHeader {
fn valid(&self) -> bool {
self.magic.load(std::sync::atomic::Ordering::Acquire) == 0xabcd1234
}
}
pub struct HzFrameHeader {
width: AtomicU32,
height: AtomicU32,
}
pub struct Ivshmem {}

View file

@ -1,139 +1,57 @@
use std::{
io::Read,
net::{TcpListener, TcpStream},
};
use byteorder::{LittleEndian, ReadBytesExt};
use minifb::{Window, WindowOptions};
mod atomic_spinlock;
mod ivshmem;
#[derive(Debug)]
enum Message {
Resize { width: u32, height: u32 },
Data {},
}
pub const MESSAGETYPE_RESIZE: u32 = 0;
pub const MESSAGETYPE_DATA: u32 = 1;
fn read_message(
stream: &mut TcpStream,
argb_buffer: &mut Vec<u32>,
width: u32,
height: u32,
) -> Option<Message> {
let message_type = stream.read_u32::<LittleEndian>().expect("fuck");
let message_len = stream.read_u32::<LittleEndian>().expect("fuck");
match message_type {
MESSAGETYPE_RESIZE => {
if message_len != 8 {
return None;
}
let width = stream.read_u32::<LittleEndian>().expect("fuck");
let height = stream.read_u32::<LittleEndian>().expect("fuck");
Some(Message::Resize {
width: width,
height: height,
})
}
MESSAGETYPE_DATA => {
let tile_count = stream.read_u32::<LittleEndian>().expect("fuck");
//println!("{tile_count} tiles");
// tile data, painted directly onto the argb buffer. It's stupid
let argb_slice = unsafe {
std::slice::from_raw_parts_mut(
argb_buffer.as_mut_ptr() as *mut u8,
argb_buffer.len() * core::mem::size_of::<u32>(),
)
};
for i in 0..tile_count {
// tile rect
let tile_x = stream.read_u32::<LittleEndian>().expect("fuck");
let tile_y = stream.read_u32::<LittleEndian>().expect("fuck");
let tile_width = stream.read_u32::<LittleEndian>().expect("fuck");
let tile_height = stream.read_u32::<LittleEndian>().expect("fuck");
//println!("tile{i}: {tile_x} {tile_y} {tile_width}x{tile_height}");
}
for y in 0..height {
//println!("{y} {tile_y} {tile_height}");
//for x in tile_
let dest_slice = &mut argb_slice
[((y * width) * 4) as usize..((y * width + (width)) * 4) as usize];
stream.read_exact(&mut dest_slice[..]).expect("FUCK");
}
Some(Message::Data {})
}
_ => {
return None;
}
}
}
mod hzclient;
fn main() {
let listener = TcpListener::bind("192.168.1.149:9438").expect("fuck");
let (mut socket, client_addr) = listener.accept().expect("FUCK!");
// disable nagles garbage bullshit
socket.set_nodelay(true).expect("fuck tcp");
let mut screen_width: u32 = 320;
let mut screen_height: u32 = 200;
let mut window = Window::new("FbcServer", 320, 200, WindowOptions::default())
.expect("you banned forever: rules do not");
let mut argb_buffer: Vec<u32> = Vec::new();
let mut screen_width: u32 = 320;
let mut screen_height: u32 = 200;
let mut client = hzclient::HazelnutClient::new();
if !client.open("/dev/shm/lg-win7".into()) {
println!("FUCK");
} else {
println!("Opened ivshmem!!!");
}
//let mut argb_buffer: Vec<u32> = Vec::new();
loop {
if let Some(message) =
read_message(&mut socket, &mut argb_buffer, screen_width, screen_height)
{
match message {
Message::Resize { width, height } => {
println!("read message {:?}", message);
screen_width = width;
screen_height = height;
match client.tick_one() {
hzclient::ResultCode::Fail => break,
hzclient::ResultCode::Changed => {
let dims = client.dimensions();
if screen_width != dims.0 && screen_height != dims.1 {
screen_width = dims.0;
screen_height = dims.1;
window = Window::new(
"FbcServer",
width as usize,
height as usize,
screen_width as usize,
screen_height as usize,
WindowOptions::default(),
)
.expect("you banned forever: rules do not");
}
argb_buffer.resize((screen_width * screen_height) as usize, 0);
}
Message::Data {} => {
//println!("read DATA");
window
.update_with_buffer(
&argb_buffer[..],
screen_width as usize,
screen_height as usize,
)
.expect("Failed to update screen");
}
window.update_with_buffer(
&client.framebuffer(),
screen_width as usize,
screen_height as usize,
).expect("well its done anyways");
client.unlock();
}
hzclient::ResultCode::Unchanged => {
window.update();
// Not needed, C++ unlocks us
//client.unlock();
}
} else {
println!("invalid message, termination.");
break;
}
//window.upd
}
println!("Hello, world!");

130
src/rust_wrapper.cpp Normal file
View file

@ -0,0 +1,130 @@
#include "ivshmem.hpp"
#include "ivshmem_protocol.hpp"
#include "Utils.hpp"
enum class ResultCode : u32 { Unchanged, Changed, Fail };
struct HazelnutIvshmemClient {
bool Open(const char* path) {
if(!ivshmemDevice.Open(path))
return false;
auto* ptr = (u8*)ivshmemDevice.GetPointer();
pHeader = (hazelnut::IvshHeader*)&ptr[0];
pFrameHeader = (hazelnut::FrameHeader*)&ptr[0x1000];
serial = pHeader->serverSessionId.load();
lastFrameSerial = 0;
return true;
}
// THIS LEAVES THE LOCK HELD SO YOU CAN READ ON CHANGE!!!!
ResultCode TickOne() {
if(!pHeader)
return ResultCode::Fail;
if(serial != pHeader->serverSessionId.load())
return ResultCode::Fail;
if(pHeader->lock.try_lock_manually()) {
// failed to lock
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;
}
void Lock() { pHeader->lock.lock_manually(); }
void Unlock() { pHeader->lock.unlock(); }
u32* Framebuffer() {
if(pFrameHeader) {
return pFrameHeader->bits();
}
return nullptr;
}
hazelnut::IvshmemDevice ivshmemDevice;
// Protocol/memory stuff
hazelnut::IvshHeader* pHeader;
hazelnut::FrameHeader* pFrameHeader;
u32 serial {};
u32 lastFrameSerial {};
};
// Rust bindings
extern "C" {
void* rust_new_hazelnut_client() {
return (void*)new HazelnutIvshmemClient;
}
void rust_destroy_hazelnut_client(void* pClient) {
if(pClient)
delete(HazelnutIvshmemClient*)pClient;
}
bool rust_hazelnut_client_open(void* pClient, const char* pszPath) {
if(pClient) {
return ((HazelnutIvshmemClient*)pClient)->Open(pszPath);
}
return false;
}
int rust_hazelnut_client_tick(void* pClient) {
if(pClient) {
return (int)((HazelnutIvshmemClient*)pClient)->TickOne();
}
return (int)ResultCode::Fail;
}
void rust_hazelnut_client_lock(void* pClient) {
if(pClient) {
return ((HazelnutIvshmemClient*)pClient)->Lock();
}
}
void rust_hazelnut_client_unlock(void* pClient) {
if(pClient) {
return ((HazelnutIvshmemClient*)pClient)->Unlock();
}
}
u32* rust_hazelnut_client_get_framebuffer(void* pClient) {
if(pClient) {
return ((HazelnutIvshmemClient*)pClient)->Framebuffer();
}
return nullptr;
}
u32 rust_hazelnut_client_get_width(void* pClient) {
if(pClient) {
return ((HazelnutIvshmemClient*)pClient)->pFrameHeader->width.load();
}
return 0;
}
u32 rust_hazelnut_client_get_height(void* pClient) {
if(pClient) {
return ((HazelnutIvshmemClient*)pClient)->pFrameHeader->height.load();
}
return 0;
}
}