From fe5aca0ed3c91b722ed8ec17fbcde1bdff6751be Mon Sep 17 00:00:00 2001 From: FlightlessMango Date: Mon, 14 Jun 2021 22:28:02 +0200 Subject: [PATCH] Basic control implementation with hud toggle --- src/control.cpp | 199 ++++++++++++++++++++++++++++++++++++++++ src/control.h | 2 + src/mangohud-control.py | 192 ++++++++++++++++++++++++++++++++++++++ src/meson.build | 1 + src/overlay.cpp | 1 + src/overlay.h | 1 + src/vulkan.cpp | 43 ++------- src/vulkan.h | 32 +++++++ 8 files changed, 435 insertions(+), 36 deletions(-) create mode 100644 src/control.cpp create mode 100644 src/control.h create mode 100644 src/mangohud-control.py create mode 100644 src/vulkan.h diff --git a/src/control.cpp b/src/control.cpp new file mode 100644 index 00000000..d327446a --- /dev/null +++ b/src/control.cpp @@ -0,0 +1,199 @@ +#include +#include +#include +#include "mesa/util/os_socket.h" +#include "overlay.h" +#include "vulkan.h" +#include "control.h" + +using namespace std; +static void parse_command(struct instance_data *instance_data, + const char *cmd, unsigned cmdlen, + const char *param, unsigned paramlen) +{ + if (!strncmp(cmd, "hud", cmdlen)) { + params_ptr->no_display = !params_ptr->no_display; + } +} + +#define BUFSIZE 4096 + +/** + * This function will process commands through the control file. + * + * A command starts with a colon, followed by the command, and followed by an + * option '=' and a parameter. It has to end with a semi-colon. A full command + * + parameter looks like: + * + * :cmd=param; + */ +static void process_char(struct instance_data *instance_data, char c) +{ + static char cmd[BUFSIZE]; + static char param[BUFSIZE]; + + static unsigned cmdpos = 0; + static unsigned parampos = 0; + static bool reading_cmd = false; + static bool reading_param = false; + + switch (c) { + case ':': + cmdpos = 0; + parampos = 0; + reading_cmd = true; + reading_param = false; + break; + case ';': + if (!reading_cmd) + break; + cmd[cmdpos++] = '\0'; + param[parampos++] = '\0'; + parse_command(instance_data, cmd, cmdpos, param, parampos); + reading_cmd = false; + reading_param = false; + break; + case '=': + if (!reading_cmd) + break; + reading_param = true; + break; + default: + if (!reading_cmd) + break; + + if (reading_param) { + /* overflow means an invalid parameter */ + if (parampos >= BUFSIZE - 1) { + reading_cmd = false; + reading_param = false; + break; + } + + param[parampos++] = c; + } else { + /* overflow means an invalid command */ + if (cmdpos >= BUFSIZE - 1) { + reading_cmd = false; + break; + } + + cmd[cmdpos++] = c; + } + } +} + +static void control_send(struct instance_data *instance_data, + const char *cmd, unsigned cmdlen, + const char *param, unsigned paramlen) +{ + unsigned msglen = 0; + char buffer[BUFSIZE]; + + assert(cmdlen + paramlen + 3 < BUFSIZE); + + buffer[msglen++] = ':'; + + memcpy(&buffer[msglen], cmd, cmdlen); + msglen += cmdlen; + + if (paramlen > 0) { + buffer[msglen++] = '='; + memcpy(&buffer[msglen], param, paramlen); + msglen += paramlen; + buffer[msglen++] = ';'; + } + + os_socket_send(instance_data->control_client, buffer, msglen, 0); +} + +static void control_send_connection_string(struct device_data *device_data) +{ + struct instance_data *instance_data = device_data->instance; + + const char *controlVersionCmd = "MesaOverlayControlVersion"; + const char *controlVersionString = "1"; + + control_send(instance_data, controlVersionCmd, strlen(controlVersionCmd), + controlVersionString, strlen(controlVersionString)); + + const char *deviceCmd = "DeviceName"; + const char *deviceName = device_data->properties.deviceName; + + control_send(instance_data, deviceCmd, strlen(deviceCmd), + deviceName, strlen(deviceName)); + + const char *mesaVersionCmd = "MesaVersion"; + const char *mesaVersionString = "Mesa"; + + control_send(instance_data, mesaVersionCmd, strlen(mesaVersionCmd), + mesaVersionString, strlen(mesaVersionString)); + +} + +void control_client_check(struct device_data *device_data) +{ + struct instance_data *instance_data = device_data->instance; + + /* Already connected, just return. */ + if (instance_data->control_client >= 0) + return; + + int socket = os_socket_accept(instance_data->params.control); + if (socket == -1) { + if (errno != EAGAIN && errno != EWOULDBLOCK && errno != ECONNABORTED) + fprintf(stderr, "ERROR on socket: %s\n", strerror(errno)); + return; + } + + if (socket >= 0) { + os_socket_block(socket, false); + instance_data->control_client = socket; + control_send_connection_string(device_data); + } +} + +static void control_client_disconnected(struct instance_data *instance_data) +{ + os_socket_close(instance_data->control_client); + instance_data->control_client = -1; +} + +void process_control_socket(struct instance_data *instance_data) +{ + const int client = instance_data->control_client; + if (client >= 0) { + char buf[BUFSIZE]; + + while (true) { + ssize_t n = os_socket_recv(client, buf, BUFSIZE, 0); + + if (n == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + /* nothing to read, try again later */ + break; + } + + if (errno != ECONNRESET) + fprintf(stderr, "ERROR on connection: %s\n", strerror(errno)); + + control_client_disconnected(instance_data); + } else if (n == 0) { + /* recv() returns 0 when the client disconnects */ + control_client_disconnected(instance_data); + } + + for (ssize_t i = 0; i < n; i++) { + process_char(instance_data, buf[i]); + } + + /* If we try to read BUFSIZE and receive BUFSIZE bytes from the + * socket, there's a good chance that there's still more data to be + * read, so we will try again. Otherwise, simply be done for this + * iteration and try again on the next frame. + */ + if (n < BUFSIZE) + break; + } + } +} \ No newline at end of file diff --git a/src/control.h b/src/control.h new file mode 100644 index 00000000..dc704727 --- /dev/null +++ b/src/control.h @@ -0,0 +1,2 @@ +extern void control_client_check(struct device_data *device_data); +extern void process_control_socket(struct instance_data *instance_data); \ No newline at end of file diff --git a/src/mangohud-control.py b/src/mangohud-control.py new file mode 100644 index 00000000..4d98f52a --- /dev/null +++ b/src/mangohud-control.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +import os +import socket +import sys +import select +from select import EPOLLIN, EPOLLPRI, EPOLLERR +import time +from collections import namedtuple +import argparse + +TIMEOUT = 1.0 # seconds + +VERSION_HEADER = bytearray('MesaOverlayControlVersion', 'utf-8') +DEVICE_NAME_HEADER = bytearray('DeviceName', 'utf-8') +MESA_VERSION_HEADER = bytearray('MesaVersion', 'utf-8') + +DEFAULT_SERVER_ADDRESS = "\0mangohud" + +class Connection: + def __init__(self, path): + # Create a Unix Domain socket and connect + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + sock.connect(path) + except socket.error as msg: + print(msg) + sys.exit(1) + + self.sock = sock + + # initialize poll interface and register socket + epoll = select.epoll() + epoll.register(sock, EPOLLIN | EPOLLPRI | EPOLLERR) + self.epoll = epoll + + def recv(self, timeout): + ''' + timeout as float in seconds + returns: + - None on error or disconnection + - bytes() (empty) on timeout + ''' + + events = self.epoll.poll(timeout) + for ev in events: + (fd, event) = ev + if fd != self.sock.fileno(): + continue + + # check for socket error + if event & EPOLLERR: + return None + + # EPOLLIN or EPOLLPRI, just read the message + msg = self.sock.recv(4096) + + # socket disconnected + if len(msg) == 0: + return None + + return msg + + return bytes() + + def send(self, msg): + self.sock.send(msg) + +class MsgParser: + MSGBEGIN = bytes(':', 'utf-8')[0] + MSGEND = bytes(';', 'utf-8')[0] + MSGSEP = bytes('=', 'utf-8')[0] + + def __init__(self, conn): + self.cmdpos = 0 + self.parampos = 0 + self.bufferpos = 0 + self.reading_cmd = False + self.reading_param = False + self.buffer = None + self.cmd = bytearray(4096) + self.param = bytearray(4096) + + self.conn = conn + + def readCmd(self, ncmds, timeout=TIMEOUT): + ''' + returns: + - None on error or disconnection + - bytes() (empty) on timeout + ''' + + parsed = [] + + remaining = timeout + + while remaining > 0 and ncmds > 0: + now = time.monotonic() + + if self.buffer == None: + self.buffer = self.conn.recv(remaining) + self.bufferpos = 0 + + # disconnected or error + if self.buffer == None: + return None + + for i in range(self.bufferpos, len(self.buffer)): + c = self.buffer[i] + self.bufferpos += 1 + if c == self.MSGBEGIN: + self.cmdpos = 0 + self.parampos = 0 + self.reading_cmd = True + self.reading_param = False + elif c == self.MSGEND: + if not self.reading_cmd: + continue + self.reading_cmd = False + self.reading_param = False + + cmd = self.cmd[0:self.cmdpos] + param = self.param[0:self.parampos] + self.reading_cmd = False + self.reading_param = False + + parsed.append((cmd, param)) + ncmds -= 1 + if ncmds == 0: + break + elif c == self.MSGSEP: + if self.reading_cmd: + self.reading_param = True + else: + if self.reading_param: + self.param[self.parampos] = c + self.parampos += 1 + elif self.reading_cmd: + self.cmd[self.cmdpos] = c + self.cmdpos += 1 + + # if we read the entire buffer and didn't finish the command, + # throw it away + self.buffer = None + + # check if we have time for another iteration + elapsed = time.monotonic() - now + remaining = max(0, remaining - elapsed) + + # timeout + return parsed + +def control(args): + if args.socket: + address = '\0' + args.socket + else: + address = DEFAULT_SERVER_ADDRESS + + conn = Connection(address) + msgparser = MsgParser(conn) + + version = None + name = None + mesa_version = None + + msgs = msgparser.readCmd(3) + + if args.info: + info = "Protocol Version: {}\n" + info += "Device Name: {}\n" + info += "Mesa Version: {}" + print(info.format(version, name, mesa_version)) + + if args.cmd == 'start-capture': + conn.send(bytearray(':capture=1;', 'utf-8')) + elif args.cmd == 'stop-capture': + conn.send(bytearray(':capture=0;', 'utf-8')) + elif args.cmd == 'toggle-hud': + conn.send(bytearray(':hud;', 'utf-8')) + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='MESA_overlay control client') + parser.add_argument('--info', action='store_true', help='Print info from socket') + parser.add_argument('--socket', '-s', type=str, help='Path to socket') + + commands = parser.add_subparsers(help='commands to run', dest='cmd') + commands.add_parser('start-capture') + commands.add_parser('stop-capture') + commands.add_parser('toggle-hud') + + args = parser.parse_args() + + control(args) diff --git a/src/meson.build b/src/meson.build index 6fd11df1..9b19542e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -80,6 +80,7 @@ if is_unixy 'real_dlsym.cpp', 'pci_ids.cpp', 'battery.cpp', + 'control.cpp', ) opengl_files = files( diff --git a/src/overlay.cpp b/src/overlay.cpp index 1684a5f8..75cce27e 100644 --- a/src/overlay.cpp +++ b/src/overlay.cpp @@ -19,6 +19,7 @@ struct benchmark_stats benchmark; struct fps_limit fps_limit_stats {}; ImVec2 real_font_size; std::vector graph_data; +struct overlay_params* params_ptr; void update_hw_info(struct swapchain_stats& sw_stats, struct overlay_params& params, uint32_t vendorID) { diff --git a/src/overlay.h b/src/overlay.h index 27c8a349..76c2531c 100644 --- a/src/overlay.h +++ b/src/overlay.h @@ -86,6 +86,7 @@ extern ImVec2 real_font_size; extern std::string wineVersion; extern std::vector graph_data; extern string engineName; +extern struct overlay_params* params_ptr; void position_layer(struct swapchain_stats& data, struct overlay_params& params, ImVec2 window_size); void render_imgui(swapchain_stats& data, struct overlay_params& params, ImVec2& window_size, bool is_vulkan); diff --git a/src/vulkan.cpp b/src/vulkan.cpp index 6fca9f2d..2714b820 100644 --- a/src/vulkan.cpp +++ b/src/vulkan.cpp @@ -65,43 +65,14 @@ #include "blacklist.h" #include "pci_ids.h" #include "timing.hpp" - +#include "vulkan.h" +#include "control.h" string gpuString,wineVersion,wineProcess,engineName; float offset_x, offset_y, hudSpacing; int hudFirstRow, hudSecondRow; VkPhysicalDeviceDriverProperties driverProps = {}; int32_t deviceID; -/* Mapped from VkInstace/VkPhysicalDevice */ -struct instance_data { - struct vk_instance_dispatch_table vtable; - VkInstance instance; - struct overlay_params params; - uint32_t api_version; - string engineName, engineVersion; - notify_thread notifier; -}; - -/* Mapped from VkDevice */ -struct queue_data; -struct device_data { - struct instance_data *instance; - - PFN_vkSetDeviceLoaderData set_device_loader_data; - - struct vk_device_dispatch_table vtable; - VkPhysicalDevice physical_device; - VkDevice device; - - VkPhysicalDeviceProperties properties; - - struct queue_data *graphic_queue; - - std::vector queues; -}; - -/* Mapped from VkCommandBuffer */ -struct queue_data; struct command_buffer_data { struct device_data *device; @@ -400,6 +371,7 @@ static struct swapchain_data *new_swapchain_data(VkSwapchainKHR swapchain, data->swapchain = swapchain; data->window_size = ImVec2(instance_data->params.width, instance_data->params.height); map_object(HKEY(data->swapchain), data); + params_ptr = &instance_data->params; return data; } @@ -704,10 +676,10 @@ static void snapshot_swapchain_frame(struct swapchain_data *data) check_keybinds(data->sw_stats, instance_data->params, device_data->properties.vendorID); // not currently used - // if (instance_data->params.control >= 0) { - // control_client_check(device_data); - // process_control_socket(instance_data); - // } + if (instance_data->params.control >= 0) { + control_client_check(device_data); + process_control_socket(instance_data); + } } static void compute_swapchain_display(struct swapchain_data *data) @@ -729,7 +701,6 @@ static void compute_swapchain_display(struct swapchain_data *data) ImGui::EndFrame(); ImGui::Render(); - } static uint32_t vk_memory_type(struct device_data *data, diff --git a/src/vulkan.h b/src/vulkan.h new file mode 100644 index 00000000..611b39ce --- /dev/null +++ b/src/vulkan.h @@ -0,0 +1,32 @@ +#include +#include "mesa/util/os_socket.h" +#include "vk_enum_to_str.h" +#include "notify.h" +#include +using namespace std; + +struct instance_data { + struct vk_instance_dispatch_table vtable; + VkInstance instance; + struct overlay_params params; + uint32_t api_version; + string engineName, engineVersion; + notify_thread notifier; + int control_client; +}; + +struct device_data { + struct instance_data *instance; + + PFN_vkSetDeviceLoaderData set_device_loader_data; + + struct vk_device_dispatch_table vtable; + VkPhysicalDevice physical_device; + VkDevice device; + + VkPhysicalDeviceProperties properties; + + struct queue_data *graphic_queue; + + std::vector queues; +}; \ No newline at end of file