Compare commits

...

3 commits

Author SHA1 Message Date
758df315a1 working ivshmem setup 2024-12-01 22:08:11 -05:00
7a6ef52144 ivshmem work 2024-12-01 19:24:43 -05:00
11766f6385 clean up nvfbclibrary more 2024-12-01 01:39:48 -05:00
24 changed files with 1019 additions and 339 deletions

3
.gitignore vendored
View file

@ -5,3 +5,6 @@
/agent/bin
/agent/obj
/agent/compile_commands.json
# test stuff
/shared/src/test

12
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"
@ -82,7 +76,7 @@ checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
name = "fbcserver"
version = "0.1.0"
dependencies = [
"byteorder",
"cc",
"minifb",
]
@ -204,9 +198,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.164"
version = "0.2.167"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc"
[[package]]
name = "libloading"

View file

@ -4,5 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
byteorder = "1.5.0"
minifb = "0.27.0"
[build-dependencies]
cc = "1.0.99"

View file

@ -9,6 +9,7 @@ OBJDIR = obj/$(ARCH)/$(CONFIG)
VPATH = src/crt \
src/base \
src/ \
../shared/src
# Objects for crt
OBJS_STARTUP := $(OBJDIR)/crt0.o \
@ -21,6 +22,7 @@ OBJS_BASELIB :=
OBJS := $(OBJDIR)/capture.o \
$(OBJDIR)/capture_nvfbc.o \
$(OBJDIR)/nvfbc_library.o \
$(OBJDIR)/ivshmem.o \
$(OBJDIR)/main.o \
LINK_LIBS := $(VS2022_PATH)/ucrt/lib/$(ARCH)/libucrt$(D).lib \
@ -30,7 +32,7 @@ LINK_LIBS := $(VS2022_PATH)/ucrt/lib/$(ARCH)/libucrt$(D).lib \
$(VS2022_PATH)/winsdk/lib/$(ARCH)/kernel32.lib \
$(VS2022_PATH)/winsdk/lib/$(ARCH)/user32.lib \
$(VS2022_PATH)/winsdk/lib/$(ARCH)/comctl32.lib \
$(VS2022_PATH)/winsdk/lib/$(ARCH)/ws2_32.lib
$(VS2022_PATH)/winsdk/lib/$(ARCH)/setupapi.lib \
.PHONY: all dumpinfo clean matrix
@ -40,6 +42,11 @@ publish: $(BINDIR)/$(NAME).exe
cp -rv bin /data/sda/shit/Release/clangent
chmod -Rvv 777 /data/sda/shit/Release/clangent
publish2: $(BINDIR)/$(NAME).exe
cp -rv bin/ /data/sda/shit/Release/ivshtest/
chmod -Rvv 777 /data/sda/shit/Release/ivshtest
dumpinfo:
echo -e "\e[96mBuilding configuration $(CONFIG) for arch $(ARCH) \e[0m"

View file

@ -8,7 +8,7 @@ SDK_INCLUDES := -isystem $(VS2022_PATH)/ucrt/include -isystem $(VS2022_PATH)/crt
# Windows 6.1
BASE_CCFLAGS := $(SDK_INCLUDES) -D_WIN32_WINNT=0x0601 -Ires -std=c17
BASE_CXXFLAGS := $(SDK_INCLUDES) -D_CRT_SECURE_NO_WARNINGS -fno-exceptions -fno-rtti -D_WIN32_WINNT=0x0601 -Isrc -Ires -std=c++23
BASE_CXXFLAGS := $(SDK_INCLUDES) -D_CRT_SECURE_NO_WARNINGS -fno-exceptions -fno-rtti -D_WIN32_WINNT=0x0601 -I../shared/src -Isrc -Ires -std=c++23
Release_Valid = yes
Release_CCFLAGS = -O3 -ffast-math -DNDEBUG

View file

@ -0,0 +1,69 @@
#include <initguid.h>
DEFINE_GUID (GUID_DEVINTERFACE_IVSHMEM,
0xdf576976,0x569d,0x4672,0x95,0xa0,0xf5,0x7e,0x4e,0xa0,0xb2,0x10);
// {df576976-569d-4672-95a0-f57e4ea0b210}
typedef UINT16 IVSHMEM_PEERID;
typedef UINT64 IVSHMEM_SIZE;
#define IVSHMEM_CACHE_NONCACHED 0
#define IVSHMEM_CACHE_CACHED 1
#define IVSHMEM_CACHE_WRITECOMBINED 2
/*
This structure is for use with the IOCTL_IVSHMEM_REQUEST_MMAP IOCTL
*/
typedef struct IVSHMEM_MMAP_CONFIG
{
UINT8 cacheMode; // the caching mode of the mapping, see IVSHMEM_CACHE_* for options
}
IVSHMEM_MMAP_CONFIG, *PIVSHMEM_MMAP_CONFIG;
/*
This structure is for use with the IOCTL_IVSHMEM_REQUEST_MMAP IOCTL
*/
typedef struct IVSHMEM_MMAP
{
IVSHMEM_PEERID peerID; // our peer id
IVSHMEM_SIZE size; // the size of the memory region
PVOID ptr; // pointer to the memory region
UINT16 vectors; // the number of vectors available
}
IVSHMEM_MMAP, *PIVSHMEM_MMAP;
/*
This structure is for use with the IOCTL_IVSHMEM_RING_DOORBELL IOCTL
*/
typedef struct IVSHMEM_RING
{
IVSHMEM_PEERID peerID; // the id of the peer to ring
UINT16 vector; // the doorbell to ring
}
IVSHMEM_RING, *PIVSHMEM_RING;
/*
This structure is for use with the IOCTL_IVSHMEM_REGISTER_EVENT IOCTL
Please Note:
- The IVSHMEM driver has a hard limit of 32 events.
- Events that are singleShot are released after they have been set.
- At this time repeating events are only released when the driver device
handle is closed, closing the event handle doesn't release it from the
drivers list. While this won't cause a problem in the driver, it will
cause you to run out of event slots.
*/
typedef struct IVSHMEM_EVENT
{
UINT16 vector; // the vector that triggers the event
HANDLE event; // the event to trigger
BOOLEAN singleShot; // set to TRUE if you want the driver to only trigger this event once
}
IVSHMEM_EVENT, *PIVSHMEM_EVENT;
#define IOCTL_IVSHMEM_REQUEST_PEERID CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_IVSHMEM_REQUEST_SIZE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_IVSHMEM_REQUEST_MMAP CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_IVSHMEM_RELEASE_MMAP CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_IVSHMEM_RING_DOORBELL CTL_CODE(FILE_DEVICE_UNKNOWN, 0x804, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_IVSHMEM_REGISTER_EVENT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x805, METHOD_BUFFERED, FILE_ANY_ACCESS)

View file

@ -79,11 +79,11 @@ namespace hazelnut {
void Shutdown() {
NvfbcDestroyInstance();
nvfbcLib.close();
nvfbcLib.Close();
}
bool Initialize() override {
if(!nvfbcLib.load()) {
if(!nvfbcLib.Load()) {
return false;
}
@ -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

View file

@ -1,4 +1,5 @@
// clang-format off
#include "ivshmem.hpp"
#pragma comment(lib, "ws2_32.lib")
#include <WinSock2.h>
#include <ws2tcpip.h>
@ -11,170 +12,134 @@
#include "Utils.hpp"
// clang-format on
#pragma pack(push, 1)
enum class MessageType : u32 {
Resize, // tResizeMessage
Data, // tDataMessage
};
struct tMessageHeader {
MessageType type;
u32 datalen;
// data
};
struct tResizeMessage {
u32 width;
u32 height;
};
struct tTile {
u32 tile_x;
u32 tile_y;
u32 tile_width;
u32 tile_height;
// u32 tile_rgba[tile_width*tile_height]
};
struct tDataMessage {
u32 tileCount;
};
#pragma pack(pop)
#include "atomic_spinlock.hpp"
#include "ivshmem_protocol.hpp"
struct tileRect {
u32 x, y, width, height;
};
// client for streamserver
class cStreamClient {
SOCKET tcpSocket;
public:
cStreamClient() = default;
~cStreamClient() {
if(tcpSocket != -1) {
closesocket(tcpSocket);
tcpSocket = -1;
}
}
bool Connect(const char* address, int port) {
tcpSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(tcpSocket == -1) {
return false;
}
sockaddr_in clientSvc {};
clientSvc.sin_family = AF_INET;
inet_pton(AF_INET, address, &clientSvc.sin_addr.s_addr);
clientSvc.sin_port = htons(port);
if(connect(tcpSocket, (SOCKADDR*)&clientSvc, sizeof(clientSvc)) == SOCKET_ERROR) {
printf("No connection socket. Fuck you\n");
return false;
}
// disable Nagle's alrogithm
int nodelay = 1;
setsockopt(tcpSocket, IPPROTO_TCP, TCP_NODELAY, (const char*)&nodelay, sizeof(nodelay));
return true;
}
void SendResize(const tResizeMessage& resize) {
tMessageHeader header;
header.type = MessageType::Resize;
header.datalen = sizeof(tResizeMessage);
send(tcpSocket, (const char*)&header, sizeof(header), 0);
send(tcpSocket, (const char*)&resize, sizeof(resize), 0);
}
void SendData(const u32* pData, u32 width, u32 height, const std::vector<tileRect>& tiles, bool sendFull) {
tMessageHeader header;
header.type = MessageType::Data;
// header.datalen = data.get_size() * sizeof(UINT32);
header.datalen = sizeof(tDataMessage);
// send tile header
// bool sendFull = false;
tDataMessage dm;
// if(tiles.empty()) {
// sendFull = true;
// }
if(sendFull) {
dm.tileCount = 1;
} else {
dm.tileCount = (u32)tiles.size();
}
/*if(tiles.empty()) {
// Send the full screen as a "tile"
dm.tileCount = 1;
sendFull = true;
} else {
}*/
// we have writev() at home
// writev() at home:
send(tcpSocket, (const char*)&header, sizeof(header), 0);
send(tcpSocket, (const char*)&dm, sizeof(dm), 0);
// send each tile
if(!sendFull) {
for(auto& tile : tiles) {
tTile tile_wire { tile.x, tile.y, tile.width, tile.height };
#if 0
std::vector<u32> tileData;
tileData.resize(tile.width * tile.height);
for(u32 y = 0; y < tile.height; ++y) {
auto* pTileLineStart = &pData[(tile.y + y) * width + tile.x];
memcpy(&tileData[y * tile.width], pTileLineStart, tile.width * sizeof(u32));
}
#endif
// send header
send(tcpSocket, (const char*)&tile_wire, (i32)sizeof(tile_wire), 0);
// send(tcpSocket, (const char*)&tileData[0], (i32)tileData.size() * 4, 0);
// send data now
/*for (auto y = tile.y; y < tile.y + tile.height; ++y) {
auto* pTileLineStart = &pData[y * width + tile.x];
send(tcpSocket, (const char*)pTileLineStart, tile.width * sizeof(UINT32), 0);
}*/
}
} else {
tTile tDummyTile { 0, 0, width, height };
send(tcpSocket, (const char*)&tDummyTile, sizeof(tDummyTile), 0);
// send(tcpSocket, (const char*)&pData[0], (i32)((width * height) * sizeof(u32)), 0);
}
send(tcpSocket, (const char*)&pData[0], (i32)((width * height) * sizeof(u32)), 0);
// send(tcpSocket, (const char*)&data.data()[0], data.get_size() * sizeof(UINT32), 0);
}
struct Test {
hazelnut::AtomicSpinlock lk {};
std::atomic<u32> sessionId {};
std::atomic<u32> pingPong {};
};
int main(int argc, char** argv) {
WSADATA data;
if(WSAStartup(MAKEWORD(2, 2), &data) != NO_ERROR) {
hazelnut::IvshmemDevice dev;
if(!dev.Open()) {
printf("Failed to open ivshmem device\n");
return 1;
}
cStreamClient client;
auto size = dev.GetSize();
auto ptr = (u8*)dev.GetPointer();
if(!client.Connect("192.168.1.149", 9438)) {
printf("conn failed\n");
printf("opened a %zu MB large ivshmem\n", (size / (1024 * 1024)));
// wipe the first 1mb
memset(&ptr[0], 0, 1 * (1024 * 1024));
printf("wiped memory\n");
// sex
auto* pHeader = new(&ptr[0]) hazelnut::IvshHeader {};
auto* pFrameHeader = new(&ptr[0x1000]) hazelnut::FrameHeader {};
// reset pingpong counter
pHeader->serverSessionId.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 = pHeader->lk.lock();
Sleep(5);
}
printf("pingpong %u\n", pHeader->pingPong.load());
if(tries++ == curTries) {
tries = 0;
pHeader->pingPong.fetch_add(1);
}
}
#endif
return 0;
#if 0
printf("Hazelnut agent\n");
// Create a capture interface
@ -216,8 +181,7 @@ int main(int argc, char** argv) {
continue;
}
// send that to the server
client.SendData(framebuffer.pFramebuffer, framebuffer.width, framebuffer.height, tiles, firstFrame == false);
if(firstFrame)
firstFrame = false;
@ -227,16 +191,12 @@ int main(int argc, char** argv) {
diff = capture->GetDiffInformation();
firstFrame = true;
client.SendResize({ framebuffer.width, framebuffer.height });
// send empty frame
tiles.clear();
client.SendData(framebuffer.pFramebuffer, framebuffer.width, framebuffer.height, tiles, true);
} else {
printf("Failed to capture\n");
break;
}
}
#endif
return 0;
}

View file

@ -1,4 +1,5 @@
#include "nvfbc_library.hpp"
#include "NvFBC/nvFBCToSys.h"
namespace {
@ -29,7 +30,7 @@ namespace {
}
// pBuf must be valid from [0..MAX_PATH]
void GetNvFBCDefaultPath(char* pBuf) {
bool GetNvFBCLibraryPath(char* pBuf) {
// clang-format off
constexpr const char kPathFormatString[] =
#ifdef _WIN64
@ -41,75 +42,80 @@ namespace {
#endif
// clang-format on
if(pBuf == nullptr) [[unlikely]]
return false;
// Get the Windows system directory (there's probably a "nicer" way using
// the SH* APIs but, this was previously nastier and using %SystemRoot%. I think this is OK)
char szWinDir[MAX_PATH] {};
if(GetWindowsDirectoryA(&szWinDir[0], MAX_PATH) == 0) {
strcpy(&pBuf[0], "its fucked");
return;
return false;
}
sprintf(&pBuf[0], kPathFormatString, szWinDir);
return true;
}
} // namespace
NvFBCLibrary::~NvFBCLibrary() {
if(m_handle != nullptr)
close();
Close();
}
bool NvFBCLibrary::load() {
bool NvFBCLibrary::Load() {
if(m_handle != nullptr)
return true;
char szDefaultPath[MAX_PATH] {};
GetNvFBCDefaultPath(&szDefaultPath[0]);
char szNvfbcLibPath[MAX_PATH] {};
m_handle = LoadLibraryA(szDefaultPath);
if(!GetNvFBCLibraryPath(&szNvfbcLibPath[0]))
return false;
m_handle = LoadLibraryA(szNvfbcLibPath);
if(m_handle == nullptr) {
return false;
}
// Load NvFBC exports
pfn_create = reinterpret_cast<NvFBC_CreateFunctionExType>(GetProcAddress(m_handle, "NvFBC_CreateEx"));
pfn_set_global_flags = reinterpret_cast<NvFBC_SetGlobalFlagsType>(GetProcAddress(m_handle, "NvFBC_SetGlobalFlags"));
pfn_get_status = reinterpret_cast<NvFBC_GetStatusExFunctionType>(GetProcAddress(m_handle, "NvFBC_GetStatusEx"));
pfn_enable = reinterpret_cast<NvFBC_EnableFunctionType>(GetProcAddress(m_handle, "NvFBC_Enable"));
pNvFBC_CreateEx = reinterpret_cast<NvFBC_CreateFunctionExType>(GetProcAddress(m_handle, "NvFBC_CreateEx"));
pNvFBC_SetGlobalFlags = reinterpret_cast<NvFBC_SetGlobalFlagsType>(GetProcAddress(m_handle, "NvFBC_SetGlobalFlags"));
pNvFBC_GetStatusEx = reinterpret_cast<NvFBC_GetStatusExFunctionType>(GetProcAddress(m_handle, "NvFBC_GetStatusEx"));
pNvFBC_Enable = reinterpret_cast<NvFBC_EnableFunctionType>(GetProcAddress(m_handle, "NvFBC_Enable"));
if((pfn_create == nullptr) || (pfn_set_global_flags == nullptr) || (pfn_get_status == nullptr) || (pfn_enable == nullptr)) {
close();
if((pNvFBC_CreateEx == nullptr) || (pNvFBC_SetGlobalFlags == nullptr) || (pNvFBC_GetStatusEx == nullptr) || (pNvFBC_Enable == nullptr)) {
Close();
return false;
}
return true;
}
void NvFBCLibrary::close() {
void NvFBCLibrary::Close() {
if(m_handle != nullptr)
FreeLibrary(m_handle);
m_handle = NULL;
pfn_create = NULL;
pfn_get_status = NULL;
pfn_enable = NULL;
pNvFBC_CreateEx = NULL;
pNvFBC_GetStatusEx = NULL;
pNvFBC_Enable = NULL;
}
NVFBCRESULT NvFBCLibrary::getStatus(NvFBCStatusEx* status) {
return pfn_get_status((void*)status);
NVFBCRESULT NvFBCLibrary::GetStatus(NvFBCStatusEx* status) {
return pNvFBC_GetStatusEx((void*)status);
}
void NvFBCLibrary::setGlobalFlags(DWORD flags, int adapter) {
setTargetAdapter(adapter);
pfn_set_global_flags(flags);
void NvFBCLibrary::SetGlobalFlags(DWORD flags, int adapter) {
SetTargetAdapter(adapter);
pNvFBC_SetGlobalFlags(flags);
}
NVFBCRESULT NvFBCLibrary::createEx(NvFBCCreateParams* pParams) {
return pfn_create((void*)pParams);
NVFBCRESULT NvFBCLibrary::CreateEx(NvFBCCreateParams* pParams) {
return pNvFBC_CreateEx((void*)pParams);
}
void* NvFBCLibrary::create(DWORD type, DWORD* maxWidth, DWORD* maxHeight, int adapter, void* devicePtr) {
void* NvFBCLibrary::CreateRaw(DWORD type, DWORD* maxWidth, DWORD* maxHeight, int adapter, void* devicePtr) {
if(NULL == m_handle)
return NULL;
@ -117,7 +123,7 @@ void* NvFBCLibrary::create(DWORD type, DWORD* maxWidth, DWORD* maxHeight, int ad
NvFBCStatusEx status {};
status.dwVersion = NVFBC_STATUS_VER;
status.dwAdapterIdx = adapter;
res = getStatus(&status);
res = GetStatus(&status);
if(res != NVFBC_SUCCESS) {
return nullptr;
@ -138,23 +144,26 @@ void* NvFBCLibrary::create(DWORD type, DWORD* maxWidth, DWORD* maxHeight, int ad
createParams.pDevice = devicePtr;
createParams.dwAdapterIdx = adapter;
res = pfn_create(&createParams);
res = pNvFBC_CreateEx(&createParams);
if(maxWidth)
*maxWidth = createParams.dwMaxDisplayWidth;
if(maxHeight)
*maxHeight = createParams.dwMaxDisplayHeight;
return createParams.pNvFBC;
}
INvFBCToSys_v4* NvFBCLibrary::CreateToSys(DWORD* maxWidth, DWORD* maxHeight, int adapter) {
return static_cast<NvFBCToSys*>(create(NVFBC_TO_SYS, maxWidth, maxHeight, adapter, nullptr));
return static_cast<NvFBCToSys*>(CreateRaw(NVFBC_TO_SYS, maxWidth, maxHeight, adapter, nullptr));
}
NVFBCRESULT NvFBCLibrary::enable(NVFBC_STATE nvFBCState) {
return pfn_enable(nvFBCState);
NVFBCRESULT NvFBCLibrary::Enable(NVFBC_STATE nvFBCState) {
return pNvFBC_Enable(nvFBCState);
}
void NvFBCLibrary::setTargetAdapter(int adapter) {
void NvFBCLibrary::SetTargetAdapter(int adapter) {
char targetAdapter[10] {};
_snprintf_s(targetAdapter, 10, 9, "%d", adapter);
SetEnvironmentVariableA("NVFBC_TARGET_ADAPTER", targetAdapter);

View file

@ -18,37 +18,38 @@ class NvFBCLibrary {
~NvFBCLibrary();
/// Tries to load the NvFBC dll
bool load();
bool Load();
/// Close the NvFBC DLL.
void close();
void Close();
/// Get the status for the provided adapter,
/// if no adapter is provided the default adapter is used.
NVFBCRESULT getStatus(NvFBCStatusEx* status);
NVFBCRESULT GetStatus(NvFBCStatusEx* status);
/// Sets the global flags for the provided adapter.
/// If no adapter is provided, the default adapter is used
void setGlobalFlags(DWORD flags, int adapter = 0);
void SetGlobalFlags(DWORD flags, int adapter = 0);
/// Creates an instance of the provided NvFBC type if possible
NVFBCRESULT createEx(NvFBCCreateParams* pParams);
NVFBCRESULT CreateEx(NvFBCCreateParams* pParams);
/// Creates an instance of the provided NvFBC type if possible.
void* create(DWORD type, DWORD* maxWidth, DWORD* maxHeight, int adapter = 0, void* devicePtr = NULL);
INvFBCToSys_v4* CreateToSys(DWORD* maxWidth, DWORD* maxHeight, int adapter = 0);
/// enable/disable NVFBC
NVFBCRESULT enable(NVFBC_STATE nvFBCState);
NVFBCRESULT Enable(NVFBC_STATE nvFBCState);
protected:
void setTargetAdapter(int adapter = 0);
void SetTargetAdapter(int adapter = 0);
/// Creates an instance of the provided NvFBC type if possible.
void* CreateRaw(DWORD type, DWORD* maxWidth, DWORD* maxHeight, int adapter = 0, void* devicePtr = NULL);
private:
HMODULE m_handle { nullptr };
NvFBC_GetStatusExFunctionType pfn_get_status { nullptr };
NvFBC_SetGlobalFlagsType pfn_set_global_flags { nullptr };
NvFBC_CreateFunctionExType pfn_create { nullptr };
NvFBC_EnableFunctionType pfn_enable { nullptr };
NvFBC_GetStatusExFunctionType pNvFBC_GetStatusEx { nullptr };
NvFBC_SetGlobalFlagsType pNvFBC_SetGlobalFlags { nullptr };
NvFBC_CreateFunctionExType pNvFBC_CreateEx { nullptr };
NvFBC_EnableFunctionType pNvFBC_Enable { nullptr };
};

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

3
shared/README.md Normal file
View file

@ -0,0 +1,3 @@
# shared code
This is shared between the two ends. It is cross platform C++20.

View file

@ -1,6 +1,7 @@
#pragma once
#include <cstdint>
#include <cstddef>
// common types
using u8 = std::uint8_t;

View file

@ -0,0 +1,49 @@
#pragma once
#include <atomic>
#include <cstdio> //debugging
#include "Utils.hpp"
#pragma pack(push, 1)
namespace hazelnut {
/// A atomic spinlock. Can be held across threads, or virtual machines.
struct AtomicSpinlock {
struct LockGuard {
~LockGuard() { lock.unlock(); }
protected:
friend AtomicSpinlock;
LockGuard(AtomicSpinlock& lock) : lock(lock) {}
AtomicSpinlock& lock;
};
AtomicSpinlock() { __lock.store(0, std::memory_order::seq_cst); }
LockGuard lock() {
lock_manually();
return LockGuard(*this);
}
void lock_manually() {
u32 expected = 0;
while(!__lock.compare_exchange_strong(expected, 1, std::memory_order::seq_cst)) {
//printf("LOCK CONTENDED\n");
expected = 0;
}
}
bool try_lock_manually() {
u32 expected = 0;
return __lock.compare_exchange_strong(expected, 1, std::memory_order::seq_cst);
}
void unlock() { __lock.store(0, std::memory_order::seq_cst); }
std::atomic_uint32_t __lock;
};
} // namespace hazelnut
#pragma pack(pop)

3
shared/src/build_test.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/bash
set -xeuo pipefail
g++ -O3 -std=c++20 ivshmem.cpp test.cpp -o test

40
shared/src/ivshmem.cpp Normal file
View file

@ -0,0 +1,40 @@
#include "ivshmem.hpp"
#include <memory>
#ifdef _WIN32
#include "ivshmem_windows.cpp"
#elif __linux__
#include "ivshmem_linux.cpp"
#else
#error YOU fucked
#endif
namespace hazelnut {
IvshmemDevice::IvshmemDevice() {
pImpl = new Impl;
}
IvshmemDevice::~IvshmemDevice() {
Close();
delete pImpl;
}
bool IvshmemDevice::Open(const char* devName) {
return pImpl->Open(devName);
}
void IvshmemDevice::Close() {
pImpl->Close();
}
void* IvshmemDevice::GetPointer() {
return pImpl->GetPointer();
}
usize IvshmemDevice::GetSize() {
return pImpl->GetSize();
}
} // namespace hazelnut

31
shared/src/ivshmem.hpp Normal file
View file

@ -0,0 +1,31 @@
#pragma once
#include "Utils.hpp"
namespace hazelnut {
/// Cross-platform IVSHMEM.
struct IvshmemDevice {
IvshmemDevice();
// forbid copy/move
IvshmemDevice(const IvshmemDevice&) = delete;
IvshmemDevice(IvshmemDevice&&) = delete;
~IvshmemDevice();
/// Opens the device. On Linux [devName] MUST point to a SHMEM file.
bool Open(const char* devName = nullptr);
void Close();
/// Gets pointer to IVSHMEM memory.
void* GetPointer();
usize GetSize();
private:
struct Impl;
Impl* pImpl {};
};
} // namespace hazelnut

View file

@ -0,0 +1,82 @@
#include <dirent.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include "Utils.hpp"
namespace hazelnut {
struct IvshmemDevice::Impl {
int fd;
void* pMap = nullptr;
usize size = 0;
bool OpenDevice(const char* devName) {
// devName is not optional on Linux.
if(devName == nullptr)
return false;
int devFd = open(devName, O_RDWR, static_cast<mode_t>(0600));
if(devFd == -1)
return false;
fd = devFd;
return true;
}
bool MapMemory() {
// Device was opened, let's map memory
struct stat st {};
if(fstat(fd, &st) != 0) {
return false;
}
usize devSize = st.st_size;
void* map = mmap(0, devSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(map == MAP_FAILED) {
printf("map failed\n");
return false;
}
pMap = map;
size = devSize;
return true;
}
void UnmapMemory() {
// Unmap memory.
if(pMap)
munmap(pMap, size);
}
bool Open(const char* pDevName) {
if(!OpenDevice(pDevName))
return false;
if(!MapMemory()) {
Close();
return false;
}
return true; // mapped
}
void Close() {
UnmapMemory();
close(fd);
fd = -1;
}
void* GetPointer() { return pMap; }
usize GetSize() { return size; }
};
} // namespace hazelnut

View file

@ -0,0 +1,42 @@
#pragma once
#include <atomic>
#include "atomic_spinlock.hpp"
namespace hazelnut {
#pragma pack(push, 1)
/// Header for Hazelnut ivshmem. At 0x0 in the ivshmem memory space.
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;
// Immediately after is FrameHeader
};
/// Stored at a page boundary
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{};
// size
std::atomic<u32> width{};
std::atomic<u32> height{};
// obtain bits at the next page boundary
u32* bits() {
return reinterpret_cast<u32*>(reinterpret_cast<u8*>(this) + 0x1000);
}
};
#pragma pack(pop)
}

View file

@ -0,0 +1,168 @@
// clang-format off
#include <windows.h>
#include <setupapi.h>
// IVSHMEM ioctls
#include <virtio/ivshmem.h>
// clang-format on
namespace hazelnut {
template <class T>
struct free_deleter {
void operator()(T* ptr) {
if(ptr)
free(ptr);
}
};
// DOES NOT UNBLESS WITH ->~T() !!!
template <class T>
using unique_malloc_ptr = std::unique_ptr<T, free_deleter<T>>;
// does not bless with placement new
template <class T>
unique_malloc_ptr<T> unique_malloc(usize len) {
return { reinterpret_cast<T*>(malloc(len)), free_deleter<T> {} };
}
struct scoped_devinfo {
scoped_devinfo(HDEVINFO hDevInfo) : hDevInfo(hDevInfo) {}
~scoped_devinfo() { SetupDiDestroyDeviceInfoList(hDevInfo); }
scoped_devinfo(const scoped_devinfo&) = delete;
scoped_devinfo(scoped_devinfo&&) = delete;
operator HDEVINFO() { return hDevInfo; }
private:
HDEVINFO hDevInfo;
};
struct IvshmemDevice::Impl {
HANDLE hIvshmem { INVALID_HANDLE_VALUE };
IVSHMEM_MMAP ivshmemMmapStruct {};
bool OpenDevice() {
memset(&ivshmemMmapStruct, 0, sizeof(IVSHMEM_MMAP));
SP_DEVINFO_DATA devInfoData = {
.cbSize = sizeof(SP_DEVINFO_DATA),
};
SP_DEVICE_INTERFACE_DATA devInterfaceData { .cbSize = sizeof(SP_DEVICE_INTERFACE_DATA) };
auto hDevInfo =
scoped_devinfo { SetupDiGetClassDevsA(&GUID_DEVINTERFACE_IVSHMEM, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE) };
// just do the first one for now
SetupDiEnumDeviceInfo(hDevInfo, 0, &devInfoData);
if(auto le = GetLastError(); le != ERROR_SUCCESS && le != ERROR_NO_MORE_ITEMS) {
return false;
}
if(SetupDiEnumDeviceInterfaces(hDevInfo, &devInfoData, &GUID_DEVINTERFACE_IVSHMEM, 0, &devInterfaceData) == FALSE) {
return false;
}
DWORD dwReq = 0;
SetupDiGetDeviceInterfaceDetail(hDevInfo, &devInterfaceData, nullptr, 0, &dwReq, nullptr);
if(!dwReq) {
return false;
}
auto pData = unique_malloc<SP_DEVICE_INTERFACE_DETAIL_DATA>(dwReq);
pData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
if(!SetupDiGetDeviceInterfaceDetail(hDevInfo, &devInterfaceData, pData.get(), dwReq, nullptr, nullptr)) {
return false;
}
hIvshmem = CreateFile(pData->DevicePath, 0, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if(hIvshmem == INVALID_HANDLE_VALUE) {
return false;
}
return true;
}
template <class TInput, class TOutput>
bool Ioctl(u32 ctlCode, TInput& in, TOutput& out) {
// Needed because some versions of the IVSHMEM driver, notably the last one
// that works on Windows 7, assumes return length is not nullptr, and will crash
// the calling process if it is nullptr. Red Hat quality code everyone; it doesn't
// just envelope the Linux ecosystem - it even gets to take over the Windows one too!
DWORD dwDummyRetLength = 0;
if(!DeviceIoControl(hIvshmem, ctlCode, reinterpret_cast<void*>(&in), sizeof(TInput), reinterpret_cast<void*>(&out), sizeof(TOutput),
&dwDummyRetLength, nullptr)) {
return false;
}
return true;
}
// Ioctl() overload for bare
bool Ioctl(u32 ctlCode) {
DWORD dwDummyRetLength = 0;
if(!DeviceIoControl(hIvshmem, ctlCode, nullptr, 0, nullptr, 0, &dwDummyRetLength, nullptr)) {
return false;
}
return true;
}
bool MapMemory() {
// Device was opened, let's map memory
DWORD dwDummyRetLength = 0;
// Yay, one less syscall.
IVSHMEM_MMAP_CONFIG config = { .cacheMode = IVSHMEM_CACHE_WRITECOMBINED };
return Ioctl(IOCTL_IVSHMEM_REQUEST_MMAP, config, this->ivshmemMmapStruct);
}
void UnmapMemory() {
// Unmap memory.
if(ivshmemMmapStruct.ptr) {
// We do not care if this fails since the device will eventually be closed and this is done only
// on agent exit, so the map will be released by the kernel anyhow.
static_cast<void>(Ioctl(IOCTL_IVSHMEM_RELEASE_MMAP));
}
memset(&ivshmemMmapStruct, 0, sizeof(IVSHMEM_MMAP));
}
bool Open(const char* pDevName) {
if(!OpenDevice())
return false;
if(!MapMemory()) {
Close();
return false;
}
return true; // mapped
}
void Close() {
UnmapMemory();
CloseHandle(hIvshmem);
hIvshmem = INVALID_HANDLE_VALUE;
}
void* GetPointer() {
return ivshmemMmapStruct.ptr;
}
usize GetSize() {
return ivshmemMmapStruct.size;
}
};
} // namespace hazelnut

64
shared/src/test.cpp Normal file
View file

@ -0,0 +1,64 @@
#include <thread>
#include "atomic_spinlock.hpp"
#include "ivshmem.hpp"
#include "ivshmem_protocol.hpp"
int main(int argc, char** argv) {
hazelnut::IvshmemDevice dev;
if(!dev.Open("/dev/shm/lg-win7")) {
printf("Failed to open ivshmem device\n");
return 1;
}
auto size = dev.GetSize();
auto ptr = (u8*)dev.GetPointer();
printf("opened a %zu MB large ivshmem\n", (size / (1024 * 1024)));
auto* pHeader = (hazelnut::IvshHeader*)&ptr[0];;
auto* pFrameHeader = (hazelnut::FrameHeader*)&ptr[0x1000];
u64 initalizedSessionId = pHeader->serverSessionId.load();
u32 lastSerial = 0;
while(true) {
if(initalizedSessionId != pHeader->serverSessionId.load()) {
printf("Agent restarted. Closing\n");
break;
}
// lock for a bit
{
if(pHeader->lock.try_lock_manually()) {
// failed to lock
continue;
}
auto current = pFrameHeader->serial.load();
if(current == lastSerial) {
pHeader->lock.unlock();
continue;
}
lastSerial = current;
printf("Frame with serial %u. Width %ux%u\n", lastSerial, pFrameHeader->width.load(), pFrameHeader->height.load());
pHeader->lock.unlock();
}
// allow the vm some time to do whatever it is it wants to do
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
return 0;
}

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,136 +1,57 @@
use std::{
io::Read,
net::{TcpListener, TcpStream},
};
use byteorder::{LittleEndian, ReadBytesExt};
use minifb::{Window, WindowOptions};
#[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[..],
window.update_with_buffer(
&client.framebuffer(),
screen_width as usize,
screen_height as usize,
)
.expect("Failed to update screen");
}
}
} else {
println!("invalid message, termination.");
break;
}
).expect("well its done anyways");
//window.upd
client.unlock();
}
hzclient::ResultCode::Unchanged => {
window.update();
// Not needed, C++ unlocks us
//client.unlock();
}
}
}
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;
}
}