initial commit

This commit is contained in:
2026-06-28 09:49:44 -05:00
commit b860b031bb
16 changed files with 1078 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
* text=auto eol=lf
*.cpp text eol=lf
*.hpp text eol=lf
*.txt text eol=lf
*.md text eol=lf
*.json text eol=lf
+29
View File
@@ -0,0 +1,29 @@
build/
build-*/
cmake-build-*/
CMakeFiles/
CMakeCache.txt
cmake_install.cmake
compile_commands.json
CTestTestfile.cmake
Testing/
wayviewer
*.o
*.obj
*.a
*.so
*.dylib
*.dll
*.exe
*.out
.cache/
.idea/
.vscode/
*.swp
*.swo
*~
.DS_Store
Thumbs.db
+35
View File
@@ -0,0 +1,35 @@
cmake_minimum_required(VERSION 3.16)
project(wayviewer VERSION 0.1.0 LANGUAGES CXX)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(PkgConfig REQUIRED)
pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2)
pkg_check_modules(LIBVNCCLIENT REQUIRED IMPORTED_TARGET libvncclient)
add_executable(wayviewer
src/input.cpp
src/main.cpp
src/options.cpp
src/sdl_app.cpp
src/viewer_state.cpp
src/vnc_client.cpp
)
target_compile_options(wayviewer PRIVATE
-Wall
-Wextra
-Wpedantic
)
target_link_libraries(wayviewer PRIVATE
PkgConfig::SDL2
PkgConfig::LIBVNCCLIENT
)
+35
View File
@@ -0,0 +1,35 @@
{
"version": 3,
"configurePresets": [
{
"name": "default",
"displayName": "Default",
"generator": "Unix Makefiles",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
},
{
"name": "debug",
"displayName": "Debug",
"generator": "Unix Makefiles",
"binaryDir": "${sourceDir}/build-debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
}
],
"buildPresets": [
{
"name": "default",
"configurePreset": "default"
},
{
"name": "debug",
"configurePreset": "debug"
}
]
}
+37
View File
@@ -0,0 +1,37 @@
# wayviewer
A small Wayland-friendly VNC viewer written in C++.
## Build
Dependencies:
- CMake 3.16+
- C++17 compiler
- SDL2 development files
- libvncclient development files
- pkg-config
```sh
cmake --preset default
cmake --build --preset default
```
For a debug build:
```sh
cmake --preset debug
cmake --build --preset debug
```
## Run
```sh
./build/wayviewer HOST:5900
```
Use `./build/wayviewer --help` for all options.
## Why?
Because I needed a VNC viewer that didnt suck and so here it is. Provided with no warranty to anyone who wants to build off of this hunk of complete trash
+83
View File
@@ -0,0 +1,83 @@
#include "input.hpp"
#include <rfb/keysym.h>
#include <rfb/rfbclient.h>
#include <algorithm>
namespace wayviewer {
std::uint32_t mapSdlKey(SDL_Keycode key)
{
if (key >= 0x20 && key <= 0x7e) {
return static_cast<std::uint32_t>(key);
}
switch (key) {
case SDLK_BACKSPACE: return XK_BackSpace;
case SDLK_TAB: return XK_Tab;
case SDLK_RETURN: return XK_Return;
case SDLK_ESCAPE: return XK_Escape;
case SDLK_DELETE: return XK_Delete;
case SDLK_HOME: return XK_Home;
case SDLK_END: return XK_End;
case SDLK_PAGEUP: return XK_Page_Up;
case SDLK_PAGEDOWN: return XK_Page_Down;
case SDLK_LEFT: return XK_Left;
case SDLK_UP: return XK_Up;
case SDLK_RIGHT: return XK_Right;
case SDLK_DOWN: return XK_Down;
case SDLK_INSERT: return XK_Insert;
case SDLK_F1: return XK_F1;
case SDLK_F2: return XK_F2;
case SDLK_F3: return XK_F3;
case SDLK_F4: return XK_F4;
case SDLK_F5: return XK_F5;
case SDLK_F6: return XK_F6;
case SDLK_F7: return XK_F7;
case SDLK_F8: return XK_F8;
case SDLK_F9: return XK_F9;
case SDLK_F10: return XK_F10;
case SDLK_F11: return XK_F11;
case SDLK_F12: return XK_F12;
case SDLK_LSHIFT: return XK_Shift_L;
case SDLK_RSHIFT: return XK_Shift_R;
case SDLK_LCTRL: return XK_Control_L;
case SDLK_RCTRL: return XK_Control_R;
case SDLK_LALT: return XK_Alt_L;
case SDLK_RALT: return XK_Alt_R;
case SDLK_LGUI: return XK_Super_L;
case SDLK_RGUI: return XK_Super_R;
default: return 0;
}
}
int buttonMaskFromMouseState(std::uint32_t buttons)
{
int mask = 0;
if (buttons & SDL_BUTTON_LMASK) {
mask |= rfbButton1Mask;
}
if (buttons & SDL_BUTTON_MMASK) {
mask |= rfbButton2Mask;
}
if (buttons & SDL_BUTTON_RMASK) {
mask |= rfbButton3Mask;
}
return mask;
}
std::pair<int, int> localToRemote(int localX, int localY, int windowW, int windowH, int remoteW, int remoteH)
{
if (windowW <= 0 || windowH <= 0 || remoteW <= 0 || remoteH <= 0) {
return {0, 0};
}
int x = localX * remoteW / windowW;
int y = localY * remoteH / windowH;
x = std::max(0, std::min(remoteW - 1, x));
y = std::max(0, std::min(remoteH - 1, y));
return {x, y};
}
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <SDL.h>
#include <cstdint>
#include <utility>
namespace wayviewer {
std::uint32_t mapSdlKey(SDL_Keycode key);
int buttonMaskFromMouseState(std::uint32_t buttons);
std::pair<int, int> localToRemote(int localX, int localY, int windowW, int windowH, int remoteW, int remoteH);
}
+14
View File
@@ -0,0 +1,14 @@
#include "sdl_app.hpp"
#include <exception>
#include <iostream>
int main(int argc, char** argv)
{
try {
return wayviewer::runViewer(argc, argv);
} catch (const std::exception& e) {
std::cerr << "wayviewer: " << e.what() << '\n';
return 1;
}
}
+235
View File
@@ -0,0 +1,235 @@
#include "options.hpp"
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <termios.h>
#include <unistd.h>
namespace wayviewer {
namespace {
[[noreturn]] void usage(const char* program, int code)
{
std::ostream& out = code == 0 ? std::cout : std::cerr;
out << "Usage: " << program << " [options]\n"
<< " " << program << " --host HOST [--port PORT] [options]\n"
<< " " << program << " HOST[:PORT] [options]\n\n"
<< "Options:\n"
<< " --host HOST VNC server host\n"
<< " --port PORT VNC server port, default 5900\n"
<< " --username USER VNC username for servers that request one\n"
<< " --password PASS VNC password\n"
<< " --password-file PATH Read the first line of PATH as the VNC password\n"
<< " --quality LEVEL low, medium, or high; default medium\n"
<< " --server-scale N Ask server for 1/N framebuffer scaling if supported\n"
<< " --no-prompt Fail instead of prompting for missing values\n"
<< " --view-only Do not send keyboard or pointer input\n"
<< " --shared Ask the server for a shared session\n"
<< " --title TITLE Window title\n"
<< " --help Show this help\n";
std::exit(code);
}
int parsePort(const std::string& value)
{
try {
const int port = std::stoi(value);
if (port <= 0 || port > 65535) {
throw std::out_of_range("port");
}
return port;
} catch (const std::exception&) {
throw std::runtime_error("invalid port: " + value);
}
}
int parseScale(const std::string& value)
{
try {
const int scale = std::stoi(value);
if (scale < 1 || scale > 16) {
throw std::out_of_range("scale");
}
return scale;
} catch (const std::exception&) {
throw std::runtime_error("invalid server scale: " + value + " (expected 1 through 16)");
}
}
std::string lower(std::string value)
{
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return value;
}
Quality parseQuality(const std::string& value)
{
const std::string normalized = lower(value);
if (normalized == "low") {
return Quality::Low;
}
if (normalized == "medium" || normalized == "med") {
return Quality::Medium;
}
if (normalized == "high") {
return Quality::High;
}
throw std::runtime_error("invalid quality: " + value + " (expected low, medium, or high)");
}
std::string readPasswordFile(const std::string& path)
{
std::ifstream file(path);
if (!file) {
throw std::runtime_error("failed to open password file: " + path);
}
std::string password;
std::getline(file, password);
return password;
}
void parseTarget(Options& options, const std::string& target)
{
const auto colon = target.rfind(':');
if (colon == std::string::npos || colon == 0 || colon == target.size() - 1) {
options.host = target;
return;
}
options.host = target.substr(0, colon);
options.port = parsePort(target.substr(colon + 1));
}
std::string promptLine(const std::string& label, const std::string& defaultValue = {})
{
std::cout << label;
if (!defaultValue.empty()) {
std::cout << " [" << defaultValue << "]";
}
std::cout << ": " << std::flush;
std::string value;
std::getline(std::cin, value);
return value.empty() ? defaultValue : value;
}
std::string promptPassword(const std::string& label)
{
std::cout << label << ": " << std::flush;
termios oldTerm {};
const bool canHide = isatty(STDIN_FILENO) && tcgetattr(STDIN_FILENO, &oldTerm) == 0;
if (canHide) {
termios newTerm = oldTerm;
newTerm.c_lflag &= ~ECHO;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &newTerm);
}
std::string value;
std::getline(std::cin, value);
if (canHide) {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &oldTerm);
std::cout << '\n';
}
return value;
}
}
Options parseArgs(int argc, char** argv)
{
Options options;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
const auto requireValue = [&](const char* name) -> std::string {
if (i + 1 >= argc) {
throw std::runtime_error(std::string("missing value for ") + name);
}
return argv[++i];
};
if (arg == "--help" || arg == "-h") {
usage(argv[0], 0);
} else if (arg == "--host") {
options.host = requireValue("--host");
} else if (arg == "--port") {
options.port = parsePort(requireValue("--port"));
} else if (arg == "--username") {
options.username = requireValue("--username");
} else if (arg == "--password") {
options.password = requireValue("--password");
} else if (arg == "--password-file") {
options.password = readPasswordFile(requireValue("--password-file"));
} else if (arg == "--quality") {
options.quality = parseQuality(requireValue("--quality"));
} else if (arg == "--server-scale") {
options.serverScale = parseScale(requireValue("--server-scale"));
} else if (arg == "--no-prompt") {
options.prompt = false;
} else if (arg == "--view-only") {
options.viewOnly = true;
} else if (arg == "--shared") {
options.shared = true;
} else if (arg == "--title") {
options.title = requireValue("--title");
} else if (!arg.empty() && arg[0] == '-') {
throw std::runtime_error("unknown option: " + arg);
} else if (options.host.empty()) {
parseTarget(options, arg);
} else {
throw std::runtime_error("unexpected argument: " + arg);
}
}
if (const char* envUsername = std::getenv("WAYVIEWER_USERNAME")) {
if (options.username.empty()) {
options.username = envUsername;
}
}
if (const char* envPassword = std::getenv("WAYVIEWER_PASSWORD")) {
if (options.password.empty()) {
options.password = envPassword;
}
}
return options;
}
void promptForMissingOptions(Options& options)
{
if (!options.prompt) {
if (options.host.empty()) {
throw std::runtime_error("missing VNC host");
}
return;
}
if (options.host.empty()) {
const std::string target = promptLine("VNC host or host:port");
if (target.empty()) {
throw std::runtime_error("missing VNC host");
}
parseTarget(options, target);
}
if (options.username.empty()) {
options.username = promptLine("Username (blank if not required)");
}
if (options.password.empty()) {
options.password = promptPassword("Password");
}
}
}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <string>
namespace wayviewer {
enum class Quality {
Low,
Medium,
High,
};
struct Options {
std::string host;
int port = 5900;
std::string username;
std::string password;
Quality quality = Quality::Medium;
int serverScale = 0;
bool prompt = true;
bool viewOnly = false;
bool shared = false;
std::string title = "wayviewer";
};
Options parseArgs(int argc, char** argv);
void promptForMissingOptions(Options& options);
}
+296
View File
@@ -0,0 +1,296 @@
#include "sdl_app.hpp"
#include "input.hpp"
#include "options.hpp"
#include "viewer_state.hpp"
#include "vnc_client.hpp"
#include <SDL.h>
#include <rfb/rfbclient.h>
#include <cstddef>
#include <iostream>
#include <memory>
#include <stdexcept>
namespace wayviewer {
namespace {
struct SdlSession {
SdlSession()
{
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
throw std::runtime_error(std::string("SDL_Init failed: ") + SDL_GetError());
}
}
~SdlSession()
{
SDL_Quit();
}
};
using WindowPtr = std::unique_ptr<SDL_Window, decltype(&SDL_DestroyWindow)>;
using RendererPtr = std::unique_ptr<SDL_Renderer, decltype(&SDL_DestroyRenderer)>;
using TexturePtr = std::unique_ptr<SDL_Texture, decltype(&SDL_DestroyTexture)>;
WindowPtr createWindow(const Options& options)
{
WindowPtr window(
SDL_CreateWindow(options.title.c_str(),
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
1024,
768,
SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI),
SDL_DestroyWindow);
if (!window) {
throw std::runtime_error(std::string("SDL_CreateWindow failed: ") + SDL_GetError());
}
return window;
}
RendererPtr createRenderer(SDL_Window* window)
{
RendererPtr renderer(SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED), SDL_DestroyRenderer);
if (!renderer) {
renderer.reset(SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE));
}
if (!renderer) {
throw std::runtime_error(std::string("SDL_CreateRenderer failed: ") + SDL_GetError());
}
return renderer;
}
void connectClient(rfbClient* client, const Options& options)
{
int initArgc = 1;
char programName[] = "wayviewer";
char* initArgv[] = {programName, nullptr};
if (!rfbInitClient(client, &initArgc, initArgv)) {
throw std::runtime_error("failed to connect or authenticate to VNC server");
}
if (options.serverScale > 1 && !SendScaleSetting(client, options.serverScale)) {
std::cerr << "wayviewer: server did not accept framebuffer scale request\n";
}
SendIncrementalFramebufferUpdateRequest(client);
}
void sendPendingMotion(rfbClient* client, bool pendingMotion, int x, int y, int mask)
{
if (pendingMotion) {
SendPointerEvent(client, x, y, mask);
}
}
int handleServerMessages(rfbClient* client, ViewerState& state)
{
int handledMessages = 0;
while (handledMessages < 256) {
const int ready = WaitForMessage(client, 0);
if (ready < 0) {
state.connected = false;
break;
}
if (ready == 0) {
break;
}
if (!HandleRFBServerMessage(client)) {
state.connected = false;
break;
}
++handledMessages;
}
return handledMessages;
}
void updateTexture(TexturePtr& texture,
SDL_Renderer* renderer,
ViewerState& state,
int& textureW,
int& textureH,
int& textureBytesPerPixel,
bool& needsRender)
{
const int remoteW = state.width;
const int remoteH = state.height;
const int bytesPerPixel = state.bytesPerPixel;
if (remoteW <= 0 || remoteH <= 0) {
return;
}
if (!texture || textureW != remoteW || textureH != remoteH || textureBytesPerPixel != bytesPerPixel) {
const Uint32 textureFormat = bytesPerPixel == 2 ? SDL_PIXELFORMAT_RGB565 : SDL_PIXELFORMAT_XRGB8888;
texture.reset(SDL_CreateTexture(renderer, textureFormat, SDL_TEXTUREACCESS_STREAMING, remoteW, remoteH));
textureW = remoteW;
textureH = remoteH;
textureBytesPerPixel = bytesPerPixel;
state.dirtyRects.clear();
markDirty(state, 0, 0, remoteW, remoteH);
state.resized = false;
}
if (!texture || !state.dirty) {
return;
}
const int pitch = remoteW * bytesPerPixel;
if (state.dirtyRects.empty()) {
SDL_UpdateTexture(texture.get(), nullptr, state.framebuffer.data(), pitch);
} else {
for (const SDL_Rect& rect : state.dirtyRects) {
const auto offset = (static_cast<std::size_t>(rect.y) * remoteW + rect.x) * bytesPerPixel;
SDL_UpdateTexture(texture.get(), &rect, state.framebuffer.data() + offset, pitch);
}
}
state.dirtyRects.clear();
state.dirty = false;
needsRender = true;
}
void renderFrame(SDL_Renderer* renderer, SDL_Texture* texture, bool& needsRender)
{
if (!needsRender) {
return;
}
SDL_SetRenderDrawColor(renderer, 16, 16, 16, 255);
SDL_RenderClear(renderer);
if (texture) {
SDL_RenderCopy(renderer, texture, nullptr, nullptr);
}
SDL_RenderPresent(renderer);
needsRender = false;
}
}
int runViewer(int argc, char** argv)
{
Options options = parseArgs(argc, argv);
promptForMissingOptions(options);
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, sdlScaleFilter(options.quality));
SdlSession sdl;
WindowPtr window = createWindow(options);
RendererPtr renderer = createRenderer(window.get());
ViewerState state;
ClientContext context;
RfbClientPtr client = createClient(options, state, context);
connectClient(client.get(), options);
state.connected = true;
TexturePtr texture(nullptr, SDL_DestroyTexture);
int textureW = 0;
int textureH = 0;
int textureBytesPerPixel = 0;
int buttonMask = 0;
bool running = true;
bool needsRender = true;
while (running && state.connected) {
const int inputRemoteW = state.width;
const int inputRemoteH = state.height;
SDL_Event event;
bool handledEvent = false;
bool pendingMotion = false;
int pendingMotionX = 0;
int pendingMotionY = 0;
int pendingMotionMask = 0;
while (SDL_PollEvent(&event)) {
handledEvent = true;
switch (event.type) {
case SDL_QUIT:
running = false;
break;
case SDL_WINDOWEVENT:
if (event.window.event == SDL_WINDOWEVENT_RESIZED ||
event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED ||
event.window.event == SDL_WINDOWEVENT_EXPOSED) {
needsRender = true;
}
break;
case SDL_KEYDOWN:
case SDL_KEYUP:
if (!options.viewOnly) {
const auto key = mapSdlKey(event.key.keysym.sym);
if (key != 0) {
SendKeyEvent(client.get(), key, event.type == SDL_KEYDOWN);
}
}
break;
case SDL_MOUSEMOTION:
if (!options.viewOnly) {
int windowW = 0;
int windowH = 0;
SDL_GetWindowSize(window.get(), &windowW, &windowH);
const auto [remoteX, remoteY] = localToRemote(event.motion.x,
event.motion.y,
windowW,
windowH,
inputRemoteW,
inputRemoteH);
buttonMask = buttonMaskFromMouseState(event.motion.state);
pendingMotion = true;
pendingMotionX = remoteX;
pendingMotionY = remoteY;
pendingMotionMask = buttonMask;
}
break;
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
if (!options.viewOnly) {
int windowW = 0;
int windowH = 0;
SDL_GetWindowSize(window.get(), &windowW, &windowH);
const auto [remoteX, remoteY] = localToRemote(event.button.x,
event.button.y,
windowW,
windowH,
inputRemoteW,
inputRemoteH);
const int bit = 1 << (event.button.button - 1);
if (event.type == SDL_MOUSEBUTTONDOWN) {
buttonMask |= bit;
} else {
buttonMask &= ~bit;
}
SendPointerEvent(client.get(), remoteX, remoteY, buttonMask);
}
break;
case SDL_MOUSEWHEEL:
if (!options.viewOnly) {
int localX = 0;
int localY = 0;
SDL_GetMouseState(&localX, &localY);
int windowW = 0;
int windowH = 0;
SDL_GetWindowSize(window.get(), &windowW, &windowH);
const auto [remoteX, remoteY] = localToRemote(localX, localY, windowW, windowH, inputRemoteW, inputRemoteH);
const int wheelMask = event.wheel.y > 0 ? rfbButton4Mask : rfbButton5Mask;
SendPointerEvent(client.get(), remoteX, remoteY, wheelMask);
SendPointerEvent(client.get(), remoteX, remoteY, 0);
}
break;
default:
break;
}
}
sendPendingMotion(client.get(), pendingMotion, pendingMotionX, pendingMotionY, pendingMotionMask);
const int handledMessages = handleServerMessages(client.get(), state);
updateTexture(texture, renderer.get(), state, textureW, textureH, textureBytesPerPixel, needsRender);
renderFrame(renderer.get(), texture.get(), needsRender);
if (!handledEvent && handledMessages == 0 && !needsRender) {
SDL_Delay(1);
}
}
return 0;
}
}
+7
View File
@@ -0,0 +1,7 @@
#pragma once
namespace wayviewer {
int runViewer(int argc, char** argv);
}
+35
View File
@@ -0,0 +1,35 @@
#include "viewer_state.hpp"
#include <algorithm>
namespace wayviewer {
void markDirty(ViewerState& state, int x, int y, int w, int h)
{
if (state.width <= 0 || state.height <= 0) {
return;
}
const int left = std::max(0, x);
const int top = std::max(0, y);
const int right = std::min(state.width, x + w);
const int bottom = std::min(state.height, y + h);
if (left >= right || top >= bottom) {
return;
}
state.dirty = true;
if (state.dirtyRects.empty()) {
state.dirtyRects.push_back(SDL_Rect{left, top, right - left, bottom - top});
return;
}
SDL_Rect& merged = state.dirtyRects.front();
const int mergedLeft = std::min(merged.x, left);
const int mergedTop = std::min(merged.y, top);
const int mergedRight = std::max(merged.x + merged.w, right);
const int mergedBottom = std::max(merged.y + merged.h, bottom);
merged = SDL_Rect{mergedLeft, mergedTop, mergedRight - mergedLeft, mergedBottom - mergedTop};
}
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <SDL.h>
#include <cstdint>
#include <vector>
namespace wayviewer {
struct ViewerState {
std::vector<std::uint8_t> framebuffer;
std::vector<SDL_Rect> dirtyRects;
int width = 0;
int height = 0;
int bytesPerPixel = 4;
bool dirty = false;
bool resized = false;
bool connected = false;
};
void markDirty(ViewerState& state, int x, int y, int w, int h);
}
+178
View File
@@ -0,0 +1,178 @@
#include "vnc_client.hpp"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <stdexcept>
namespace wayviewer {
namespace {
int clientContextTag = 0;
void applyQuality(rfbClient* client, Quality quality)
{
client->appData.forceTrueColour = TRUE;
client->appData.encodingsString = "copyrect tight zrle hextile raw";
switch (quality) {
case Quality::Low:
client->appData.requestedDepth = 16;
client->appData.compressLevel = 1;
client->appData.qualityLevel = 3;
client->appData.enableJPEG = TRUE;
break;
case Quality::Medium:
client->appData.requestedDepth = 24;
client->appData.compressLevel = 5;
client->appData.qualityLevel = 7;
client->appData.enableJPEG = TRUE;
break;
case Quality::High:
client->appData.requestedDepth = 24;
client->appData.compressLevel = 9;
client->appData.qualityLevel = 9;
client->appData.enableJPEG = TRUE;
break;
}
}
void configurePixelFormat(rfbClient* client, Quality quality)
{
if (quality == Quality::Low) {
client->format.bitsPerPixel = 16;
client->format.depth = 16;
client->format.bigEndian = 0;
client->format.trueColour = TRUE;
client->format.redMax = 31;
client->format.greenMax = 63;
client->format.blueMax = 31;
client->format.redShift = 11;
client->format.greenShift = 5;
client->format.blueShift = 0;
return;
}
client->format.bitsPerPixel = 32;
client->format.depth = 24;
client->format.bigEndian = 0;
client->format.trueColour = TRUE;
client->format.redMax = 255;
client->format.greenMax = 255;
client->format.blueMax = 255;
client->format.redShift = 16;
client->format.greenShift = 8;
client->format.blueShift = 0;
}
char* getPassword(rfbClient* client)
{
const auto* context = static_cast<ClientContext*>(rfbClientGetClientData(client, &clientContextTag));
const std::string& password = context->options->password;
auto* copy = static_cast<char*>(std::malloc(password.size() + 1));
if (!copy) {
return nullptr;
}
std::memcpy(copy, password.c_str(), password.size() + 1);
return copy;
}
rfbCredential* getCredential(rfbClient* client, int credentialType)
{
if (credentialType != rfbCredentialTypeUser) {
return nullptr;
}
const auto* context = static_cast<ClientContext*>(rfbClientGetClientData(client, &clientContextTag));
auto* credential = static_cast<rfbCredential*>(std::calloc(1, sizeof(rfbCredential)));
if (!credential) {
return nullptr;
}
credential->userCredential.username = strdup(context->options->username.c_str());
credential->userCredential.password = strdup(context->options->password.c_str());
if (!credential->userCredential.username || !credential->userCredential.password) {
std::free(credential->userCredential.username);
std::free(credential->userCredential.password);
std::free(credential);
return nullptr;
}
return credential;
}
rfbBool allocateFramebuffer(rfbClient* client)
{
auto* context = static_cast<ClientContext*>(rfbClientGetClientData(client, &clientContextTag));
auto& state = *context->state;
const int width = client->width;
const int height = client->height;
if (width <= 0 || height <= 0) {
return FALSE;
}
state.width = width;
state.height = height;
state.bytesPerPixel = std::max(1, client->format.bitsPerPixel / 8);
state.framebuffer.assign(static_cast<std::size_t>(width) * height * state.bytesPerPixel, 0);
client->frameBuffer = state.framebuffer.data();
state.dirtyRects.clear();
markDirty(state, 0, 0, width, height);
state.resized = true;
return TRUE;
}
void framebufferUpdate(rfbClient* client, int x, int y, int w, int h)
{
const auto* context = static_cast<ClientContext*>(rfbClientGetClientData(client, &clientContextTag));
markDirty(*context->state, x, y, w, h);
}
void finishedFramebufferUpdate(rfbClient* client)
{
SendIncrementalFramebufferUpdateRequest(client);
}
}
const char* sdlScaleFilter(Quality quality)
{
switch (quality) {
case Quality::Low: return "nearest";
case Quality::Medium: return "linear";
case Quality::High: return "linear";
}
return "linear";
}
RfbClientPtr createClient(Options& options, ViewerState& state, ClientContext& context)
{
const int clientBytesPerPixel = options.quality == Quality::Low ? 2 : 4;
const int bitsPerSample = options.quality == Quality::Low ? 5 : 8;
RfbClientPtr client(rfbGetClient(bitsPerSample, 3, clientBytesPerPixel), rfbClientCleanup);
if (!client) {
throw std::runtime_error("failed to allocate VNC client");
}
context.options = &options;
context.state = &state;
rfbClientSetClientData(client.get(), &clientContextTag, &context);
client->serverHost = strdup(options.host.c_str());
client->serverPort = options.port;
client->appData.shareDesktop = options.shared ? TRUE : FALSE;
client->appData.viewOnly = options.viewOnly ? TRUE : FALSE;
client->appData.scaleSetting = options.serverScale;
applyQuality(client.get(), options.quality);
client->MallocFrameBuffer = allocateFramebuffer;
client->GotFrameBufferUpdate = framebufferUpdate;
client->FinishedFrameBufferUpdate = finishedFramebufferUpdate;
client->GetPassword = getPassword;
client->GetCredential = getCredential;
configurePixelFormat(client.get(), options.quality);
return client;
}
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "options.hpp"
#include "viewer_state.hpp"
#include <rfb/rfbclient.h>
#include <memory>
namespace wayviewer {
struct ClientContext {
Options* options = nullptr;
ViewerState* state = nullptr;
};
using RfbClientPtr = std::unique_ptr<rfbClient, decltype(&rfbClientCleanup)>;
const char* sdlScaleFilter(Quality quality);
RfbClientPtr createClient(Options& options, ViewerState& state, ClientContext& context);
}