mirror of
https://github.com/miyu0-bit/frameeye-helper.git
synced 2026-10-06 06:00:26 +02:00
first version
This commit is contained in:
commit
3d43bfe5ca
47 files changed
+25167
No files matched your search
@@ -0,0 +1,62 @@
|
||||
# frameeyeosc-panel as a desktop window, for previewing it on a Linux PC without SteamVR.
|
||||
# Builds the panel's own sources unchanged, except:
|
||||
# - vr_overlay.cpp / vk_texture.cpp (SteamVR and its Vulkan textures) are replaced by vr_overlay_desktop.cpp (SDL2)
|
||||
# - config.h's font paths point at this distribution's Noto Sans CJK (SteamOS keeps them elsewhere)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(frameeyeosc-panel-preview LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE Release)
|
||||
endif()
|
||||
|
||||
set(UPSTREAM "${CMAKE_CURRENT_SOURCE_DIR}/../../upstream/frameeyeosc" CACHE PATH "frameeyeosc source tree")
|
||||
set(PANEL_FONT_REGULAR "/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc" CACHE FILEPATH "Regular font")
|
||||
set(PANEL_FONT_BOLD "/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Bold.ttc" CACHE FILEPATH "Bold font")
|
||||
|
||||
# A copy of the panel's sources with the font paths replaced
|
||||
set(SRC "${CMAKE_BINARY_DIR}/panel-src")
|
||||
file(REMOVE_RECURSE "${SRC}")
|
||||
file(COPY "${UPSTREAM}/panel/src/" DESTINATION "${SRC}")
|
||||
file(READ "${SRC}/config.h" CONFIG_H)
|
||||
string(REPLACE "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc" "${PANEL_FONT_REGULAR}" CONFIG_H "${CONFIG_H}")
|
||||
string(REPLACE "/usr/share/fonts/noto-cjk/NotoSansCJK-Bold.ttc" "${PANEL_FONT_BOLD}" CONFIG_H "${CONFIG_H}")
|
||||
file(WRITE "${SRC}/config.h" "${CONFIG_H}")
|
||||
|
||||
file(STRINGS "${UPSTREAM}/Cargo.toml" CARGO_VERSION_LINE REGEX "^version = \"[^\"]+\"" LIMIT_COUNT 1)
|
||||
string(REGEX REPLACE "^version = \"([^\"]+)\".*" "\\1" FRAMEEYEOSC_VERSION "${CARGO_VERSION_LINE}")
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
pkg_check_modules(CAIRO REQUIRED IMPORTED_TARGET cairo)
|
||||
pkg_check_modules(FREETYPE REQUIRED IMPORTED_TARGET freetype2)
|
||||
pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2)
|
||||
# vk_texture.h (included by vr_overlay.h) needs the Vulkan headers only; nothing links Vulkan
|
||||
find_path(VULKAN_INCLUDE vulkan/vulkan.h REQUIRED)
|
||||
|
||||
add_executable(frameeyeosc-panel-preview
|
||||
${SRC}/main.cpp
|
||||
${SRC}/autostart.cpp
|
||||
${SRC}/command.cpp
|
||||
${SRC}/config.cpp
|
||||
${SRC}/draw.cpp
|
||||
${SRC}/gaze_dots.cpp
|
||||
${SRC}/gaze_fit.cpp
|
||||
${SRC}/fit_text.cpp
|
||||
${SRC}/host_entry.cpp
|
||||
${SRC}/i18n.cpp
|
||||
${SRC}/json.cpp
|
||||
${SRC}/model.cpp
|
||||
${SRC}/panel.cpp
|
||||
${SRC}/sounds.cpp
|
||||
${SRC}/status.cpp
|
||||
${SRC}/target.cpp
|
||||
${SRC}/theme.cpp
|
||||
${UPSTREAM}/vendor/frame-updater/cpp/update_check.cpp
|
||||
vr_overlay_desktop.cpp
|
||||
)
|
||||
target_include_directories(frameeyeosc-panel-preview PRIVATE "${SRC}" "${UPSTREAM}/vendor/frame-updater/cpp" "${VULKAN_INCLUDE}")
|
||||
target_compile_definitions(frameeyeosc-panel-preview PRIVATE FRAMEEYEOSC_VERSION="${FRAMEEYEOSC_VERSION}")
|
||||
target_compile_options(frameeyeosc-panel-preview PRIVATE -Wall -Wextra)
|
||||
target_link_libraries(frameeyeosc-panel-preview PRIVATE PkgConfig::CAIRO PkgConfig::FREETYPE PkgConfig::SDL2 Threads::Threads)
|
||||
@@ -0,0 +1,534 @@
|
||||
// A desktop stand-in for frameeyeosc-panel's vr_overlay.cpp: the same panel in an ordinary window (SDL2)
|
||||
// instead of on the SteamVR dashboard, so it can be tried on a PC without a headset. Everything else
|
||||
// (drawing, buttons, settings, the eye fit, the sounds) is frameeyeosc-panel's own code, unchanged.
|
||||
//
|
||||
// While "the dashboard is open" the window shows the panel and the mouse works like the laser pointer.
|
||||
// Space (or the button at the top) closes the dashboard: the window then shows what you would see in the
|
||||
// headset, which is where the eye fit's dot and the debug gaze dots appear.
|
||||
|
||||
#include "vr_overlay.h"
|
||||
|
||||
#include "gaze_fit.h"
|
||||
|
||||
#include <SDL.h>
|
||||
#include <cairo.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// vk_texture.cpp needs OpenVR, so it isn't built here; VrOverlay only holds these as (unused) members.
|
||||
VulkanContext::~VulkanContext() {}
|
||||
OverlayTexture::~OverlayTexture() {}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kBar = 46; ///< the toolbar above the panel (logical px)
|
||||
constexpr double kPxPerDegree = 12.0; ///< the headset view: 100 degrees across the 1200 px panel width
|
||||
constexpr double kTargetWidthM = 0.3; ///< the eye fit's target is 0.3 m wide, 2 m ahead (see vr_overlay.cpp)
|
||||
constexpr double kTargetDistanceM = 2.0;
|
||||
|
||||
struct Image {
|
||||
std::vector<uint8_t> rgba; ///< non-premultiplied
|
||||
int size = 0;
|
||||
cairo_surface_t* surface = nullptr;
|
||||
|
||||
void set(const uint8_t* data, int edge) {
|
||||
rgba.assign(data, data + static_cast<size_t>(edge) * edge * 4);
|
||||
size = edge;
|
||||
if (surface != nullptr) cairo_surface_destroy(surface);
|
||||
surface = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
struct Dot {
|
||||
bool shown = false;
|
||||
double x = 0, y = 0, z = -1, width = 0.02;
|
||||
Image image;
|
||||
};
|
||||
|
||||
struct Desktop {
|
||||
SDL_Window* window = nullptr;
|
||||
SDL_Renderer* renderer = nullptr;
|
||||
SDL_Texture* texture = nullptr;
|
||||
cairo_surface_t* frame = nullptr; ///< the whole window: toolbar and view
|
||||
int width = 0; ///< logical size
|
||||
int height = 0;
|
||||
int panelHeight = 0;
|
||||
cairo_surface_t* panel = nullptr;
|
||||
bool dashboardOpen = true;
|
||||
bool pointerInPanel = false;
|
||||
bool buttonHover = false;
|
||||
bool dirty = true;
|
||||
Uint32 lastDraw = 0;
|
||||
bool targetShown = false;
|
||||
double targetYaw = 0, targetPitch = 0;
|
||||
Image target;
|
||||
Dot dots[2];
|
||||
} gDesk;
|
||||
|
||||
/** Non-premultiplied RGBA into a cairo (premultiplied, native-endian ARGB) surface. */
|
||||
cairo_surface_t* toSurface(const uint8_t* rgba, int width, int height) {
|
||||
cairo_surface_t* surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
|
||||
cairo_surface_flush(surface);
|
||||
unsigned char* data = cairo_image_surface_get_data(surface);
|
||||
const int stride = cairo_image_surface_get_stride(surface);
|
||||
for (int y = 0; y < height; ++y) {
|
||||
uint32_t* row = reinterpret_cast<uint32_t*>(data + static_cast<size_t>(y) * stride);
|
||||
const uint8_t* in = rgba + static_cast<size_t>(y) * width * 4;
|
||||
for (int x = 0; x < width; ++x, in += 4) {
|
||||
const uint32_t a = in[3];
|
||||
row[x] = (a << 24) | ((in[0] * a / 255) << 16) | ((in[1] * a / 255) << 8) | (in[2] * a / 255);
|
||||
}
|
||||
}
|
||||
cairo_surface_mark_dirty(surface);
|
||||
return surface;
|
||||
}
|
||||
|
||||
cairo_surface_t* surfaceOf(Image& image) {
|
||||
if (image.surface == nullptr && image.size > 0) image.surface = toSurface(image.rgba.data(), image.size, image.size);
|
||||
return image.surface;
|
||||
}
|
||||
|
||||
void rgb(cairo_t* cr, uint32_t hex, double alpha = 1.0) {
|
||||
cairo_set_source_rgba(cr, ((hex >> 16) & 0xff) / 255.0, ((hex >> 8) & 0xff) / 255.0, (hex & 0xff) / 255.0, alpha);
|
||||
}
|
||||
|
||||
void text(cairo_t* cr, double x, double y, const char* utf8, double size, uint32_t color, bool bold = false,
|
||||
bool centered = false) {
|
||||
cairo_select_font_face(cr, "Noto Sans", CAIRO_FONT_SLANT_NORMAL, bold ? CAIRO_FONT_WEIGHT_BOLD : CAIRO_FONT_WEIGHT_NORMAL);
|
||||
cairo_set_font_size(cr, size);
|
||||
if (centered) {
|
||||
cairo_text_extents_t extents;
|
||||
cairo_text_extents(cr, utf8, &extents);
|
||||
x -= extents.width / 2 + extents.x_bearing;
|
||||
}
|
||||
rgb(cr, color);
|
||||
cairo_move_to(cr, x, y);
|
||||
cairo_show_text(cr, utf8);
|
||||
}
|
||||
|
||||
/** The toolbar button's rectangle (logical px). */
|
||||
void buttonRect(double& x, double& y, double& w, double& h) {
|
||||
w = 250;
|
||||
h = 32;
|
||||
x = gDesk.width - w - 10;
|
||||
y = (kBar - h) / 2;
|
||||
}
|
||||
|
||||
bool inButton(double px, double py) {
|
||||
double x, y, w, h;
|
||||
buttonRect(x, y, w, h);
|
||||
return px >= x && px <= x + w && py >= y && py <= y + h;
|
||||
}
|
||||
|
||||
void roundedRect(cairo_t* cr, double x, double y, double w, double h, double r) {
|
||||
cairo_new_sub_path(cr);
|
||||
cairo_arc(cr, x + w - r, y + r, r, -M_PI / 2, 0);
|
||||
cairo_arc(cr, x + w - r, y + h - r, r, 0, M_PI / 2);
|
||||
cairo_arc(cr, x + r, y + h - r, r, M_PI / 2, M_PI);
|
||||
cairo_arc(cr, x + r, y + r, r, M_PI, 3 * M_PI / 2);
|
||||
cairo_close_path(cr);
|
||||
}
|
||||
|
||||
/** Draw an image centered at a direction in front of you, `degrees` wide. */
|
||||
void drawInView(cairo_t* cr, cairo_surface_t* image, int size, double yawDeg, double pitchDeg, double degrees) {
|
||||
if (image == nullptr || size <= 0) return;
|
||||
const double cx = gDesk.width / 2.0 + yawDeg * kPxPerDegree;
|
||||
const double cy = kBar + gDesk.panelHeight / 2.0 - pitchDeg * kPxPerDegree;
|
||||
const double px = degrees * kPxPerDegree;
|
||||
cairo_save(cr);
|
||||
cairo_translate(cr, cx - px / 2, cy - px / 2);
|
||||
cairo_scale(cr, px / size, px / size);
|
||||
cairo_set_source_surface(cr, image, 0, 0);
|
||||
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_GOOD);
|
||||
cairo_paint(cr);
|
||||
cairo_restore(cr);
|
||||
}
|
||||
|
||||
void drawHeadsetView(cairo_t* cr) {
|
||||
const double top = kBar;
|
||||
const double h = gDesk.panelHeight;
|
||||
const double w = gDesk.width;
|
||||
cairo_pattern_t* sky = cairo_pattern_create_radial(w / 2, top + h / 2, 40, w / 2, top + h / 2, w * 0.75);
|
||||
cairo_pattern_add_color_stop_rgb(sky, 0, 0.09, 0.11, 0.15);
|
||||
cairo_pattern_add_color_stop_rgb(sky, 1, 0.02, 0.03, 0.05);
|
||||
cairo_rectangle(cr, 0, top, w, h);
|
||||
cairo_set_source(cr, sky);
|
||||
cairo_fill(cr);
|
||||
cairo_pattern_destroy(sky);
|
||||
// A faint grid every 10 degrees, brighter through the middle
|
||||
cairo_set_line_width(cr, 1);
|
||||
for (int degree = -50; degree <= 50; degree += 10) {
|
||||
rgb(cr, degree == 0 ? 0x3a4350 : 0x1c222b);
|
||||
const double x = w / 2 + degree * kPxPerDegree + 0.5;
|
||||
cairo_move_to(cr, x, top);
|
||||
cairo_line_to(cr, x, top + h);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
for (int degree = -30; degree <= 30; degree += 10) {
|
||||
rgb(cr, degree == 0 ? 0x3a4350 : 0x1c222b);
|
||||
const double y = top + h / 2 - degree * kPxPerDegree + 0.5;
|
||||
cairo_move_to(cr, 0, y);
|
||||
cairo_line_to(cr, w, y);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
text(cr, w / 2 + 6, top + 18, "straight ahead", 12, 0x5d6672);
|
||||
text(cr, 8, top + h / 2 - 6, "eye level", 12, 0x5d6672);
|
||||
|
||||
if (gDesk.targetShown) {
|
||||
const double degrees = 2 * std::atan(kTargetWidthM / 2 / kTargetDistanceM) * 180 / M_PI;
|
||||
drawInView(cr, surfaceOf(gDesk.target), gDesk.target.size, gDesk.targetYaw, gDesk.targetPitch, degrees);
|
||||
}
|
||||
bool anyDot = false;
|
||||
for (Dot& dot : gDesk.dots) {
|
||||
if (!dot.shown) continue;
|
||||
anyDot = true;
|
||||
const double distance = std::sqrt(dot.x * dot.x + dot.y * dot.y + dot.z * dot.z);
|
||||
const double yaw = std::atan2(dot.x, -dot.z) * 180 / M_PI;
|
||||
const double pitch = std::atan2(dot.y, std::sqrt(dot.x * dot.x + dot.z * dot.z)) * 180 / M_PI;
|
||||
const double degrees = 2 * std::atan(dot.width / 2 / std::max(distance, 0.05)) * 180 / M_PI;
|
||||
drawInView(cr, surfaceOf(dot.image), dot.image.size, yaw, pitch, std::max(degrees, 1.2));
|
||||
}
|
||||
|
||||
const double bottom = top + h;
|
||||
text(cr, w / 2, bottom - 44, "The dashboard is closed: this is what you would see in the headset.", 16, 0xc9d1d9, true, true);
|
||||
if (gDesk.targetShown) {
|
||||
text(cr, w / 2, bottom - 20, "The eye fit is running. The pretend eyes follow the dot, like you would with yours.", 14, 0x9198a1,
|
||||
false, true);
|
||||
} else if (anyDot) {
|
||||
text(cr, w / 2, bottom - 20, "The dots show where the eyes being sent to VRChat are looking (Show gaze dots is on).", 14,
|
||||
0x9198a1, false, true);
|
||||
} else {
|
||||
text(cr, w / 2, bottom - 20,
|
||||
"Press Space to open the dashboard again. (Eye fit tab: press Fit my eyes, then close the dashboard.)", 14,
|
||||
0x9198a1, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
void redraw() {
|
||||
if (gDesk.renderer == nullptr) return;
|
||||
cairo_t* cr = cairo_create(gDesk.frame);
|
||||
// Toolbar
|
||||
rgb(cr, 0x0d1117);
|
||||
cairo_rectangle(cr, 0, 0, gDesk.width, kBar);
|
||||
cairo_fill(cr);
|
||||
rgb(cr, 0x30363d);
|
||||
cairo_rectangle(cr, 0, kBar - 1, gDesk.width, 1);
|
||||
cairo_fill(cr);
|
||||
text(cr, 14, 20, "Eye panel preview", 15, 0xe6edf3, true);
|
||||
text(cr, 14, 38, "A pretend Steam Frame on this PC. Nothing is sent to VRChat.", 12.5, 0x9198a1);
|
||||
double bx, by, bw, bh;
|
||||
buttonRect(bx, by, bw, bh);
|
||||
roundedRect(cr, bx, by, bw, bh, 7);
|
||||
rgb(cr, gDesk.buttonHover ? 0xc45fe0 : 0xe27dfd);
|
||||
cairo_fill(cr);
|
||||
text(cr, bx + bw / 2, by + 21, gDesk.dashboardOpen ? "Close the dashboard (Space)" : "Open the dashboard (Space)", 14,
|
||||
0x0d1117, true, true);
|
||||
// The view
|
||||
if (gDesk.dashboardOpen) {
|
||||
rgb(cr, 0x0d1117);
|
||||
cairo_rectangle(cr, 0, kBar, gDesk.width, gDesk.panelHeight);
|
||||
cairo_fill(cr);
|
||||
if (gDesk.panel != nullptr) {
|
||||
cairo_set_source_surface(cr, gDesk.panel, 0, kBar);
|
||||
cairo_paint(cr);
|
||||
}
|
||||
} else {
|
||||
drawHeadsetView(cr);
|
||||
}
|
||||
cairo_destroy(cr);
|
||||
cairo_surface_flush(gDesk.frame);
|
||||
SDL_UpdateTexture(gDesk.texture, nullptr, cairo_image_surface_get_data(gDesk.frame),
|
||||
cairo_image_surface_get_stride(gDesk.frame));
|
||||
SDL_SetRenderDrawColor(gDesk.renderer, 0x0d, 0x11, 0x17, 0xff);
|
||||
SDL_RenderClear(gDesk.renderer);
|
||||
SDL_RenderCopy(gDesk.renderer, gDesk.texture, nullptr, nullptr);
|
||||
SDL_RenderPresent(gDesk.renderer);
|
||||
gDesk.dirty = false;
|
||||
gDesk.lastDraw = SDL_GetTicks();
|
||||
}
|
||||
|
||||
/**
|
||||
* For testing and for taking pictures: PREVIEW_SCRIPT runs simple steps separated by ";":
|
||||
* wait SECONDS | click X,Y (panel px) | key space | shot FILE.png | quit
|
||||
*/
|
||||
struct Script {
|
||||
std::vector<std::string> steps;
|
||||
size_t next = 0;
|
||||
Uint32 resumeAt = 0;
|
||||
bool loaded = false;
|
||||
} gScript;
|
||||
|
||||
void setDashboard(bool open, VrEvents& events);
|
||||
|
||||
void runScript(VrEvents& events) {
|
||||
if (!gScript.loaded) {
|
||||
gScript.loaded = true;
|
||||
if (const char* script = std::getenv("PREVIEW_SCRIPT")) {
|
||||
std::string all = script;
|
||||
size_t start = 0;
|
||||
while (start <= all.size()) {
|
||||
const size_t end = all.find(';', start);
|
||||
std::string step = all.substr(start, end == std::string::npos ? std::string::npos : end - start);
|
||||
const size_t first = step.find_first_not_of(" \t");
|
||||
if (first != std::string::npos) gScript.steps.push_back(step.substr(first));
|
||||
if (end == std::string::npos) break;
|
||||
start = end + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (gScript.next < gScript.steps.size() && SDL_GetTicks() >= gScript.resumeAt) {
|
||||
const std::string step = gScript.steps[gScript.next++];
|
||||
double x = 0, y = 0, seconds = 0;
|
||||
char path[512] = {};
|
||||
if (std::sscanf(step.c_str(), "wait %lf", &seconds) == 1) {
|
||||
gScript.resumeAt = SDL_GetTicks() + static_cast<Uint32>(seconds * 1000);
|
||||
} else if (std::sscanf(step.c_str(), "click %lf,%lf", &x, &y) == 2) {
|
||||
events.pointer.push_back({PointerInput::Type::Move, x, y});
|
||||
events.pointer.push_back({PointerInput::Type::Down, x, y});
|
||||
events.pointer.push_back({PointerInput::Type::Up, x, y});
|
||||
gScript.resumeAt = SDL_GetTicks() + 150;
|
||||
} else if (step == "key space") {
|
||||
setDashboard(!gDesk.dashboardOpen, events);
|
||||
} else if (std::sscanf(step.c_str(), "shot %511s", path) == 1) {
|
||||
redraw();
|
||||
const cairo_status_t result = cairo_surface_write_to_png(gDesk.frame, path);
|
||||
std::fprintf(stderr, "[preview] %s %s\n", result == CAIRO_STATUS_SUCCESS ? "saved" : "could not save", path);
|
||||
} else if (step == "quit") {
|
||||
events.closeRequested = true;
|
||||
} else {
|
||||
std::fprintf(stderr, "[preview] unknown script step: %s\n", step.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setDashboard(bool open, VrEvents& events) {
|
||||
if (gDesk.dashboardOpen == open) return;
|
||||
gDesk.dashboardOpen = open;
|
||||
if (!open && gDesk.pointerInPanel) {
|
||||
events.pointer.push_back({PointerInput::Type::Leave, 0, 0});
|
||||
gDesk.pointerInPanel = false;
|
||||
}
|
||||
std::fprintf(stderr, "[preview] dashboard %s\n", open ? "opened" : "closed");
|
||||
gDesk.dirty = true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
VrOverlay::VrOverlay() = default;
|
||||
|
||||
VrOverlay::~VrOverlay() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
VrOverlay::ConnectResult VrOverlay::connect(int width, int height, std::string& message) {
|
||||
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
|
||||
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
|
||||
message = std::string("SDL: ") + SDL_GetError();
|
||||
return ConnectResult::Error;
|
||||
}
|
||||
gDesk.width = width;
|
||||
gDesk.panelHeight = height;
|
||||
gDesk.height = height + kBar;
|
||||
gDesk.window = SDL_CreateWindow("Eye panel preview (frameeyeosc)", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||
gDesk.width, gDesk.height, SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
|
||||
if (gDesk.window == nullptr) {
|
||||
message = std::string("SDL window: ") + SDL_GetError();
|
||||
SDL_Quit();
|
||||
return ConnectResult::Error;
|
||||
}
|
||||
gDesk.renderer = SDL_CreateRenderer(gDesk.window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
|
||||
if (gDesk.renderer == nullptr) gDesk.renderer = SDL_CreateRenderer(gDesk.window, -1, SDL_RENDERER_SOFTWARE);
|
||||
if (gDesk.renderer == nullptr) {
|
||||
message = std::string("SDL renderer: ") + SDL_GetError();
|
||||
return ConnectResult::Error;
|
||||
}
|
||||
SDL_RenderSetLogicalSize(gDesk.renderer, gDesk.width, gDesk.height);
|
||||
gDesk.texture = SDL_CreateTexture(gDesk.renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, gDesk.width,
|
||||
gDesk.height);
|
||||
gDesk.frame = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, gDesk.width, gDesk.height);
|
||||
connected_ = true;
|
||||
panelHeight_ = height;
|
||||
std::fprintf(stderr, "[preview] window %dx%d (the panel plus a %d px toolbar)\n", gDesk.width, gDesk.height, kBar);
|
||||
redraw();
|
||||
return ConnectResult::Ok;
|
||||
}
|
||||
|
||||
void VrOverlay::shutdown() {
|
||||
if (!connected_) return;
|
||||
connected_ = false;
|
||||
for (Image* image : {&gDesk.target, &gDesk.dots[0].image, &gDesk.dots[1].image}) {
|
||||
if (image->surface != nullptr) cairo_surface_destroy(image->surface);
|
||||
image->surface = nullptr;
|
||||
}
|
||||
if (gDesk.panel != nullptr) cairo_surface_destroy(gDesk.panel);
|
||||
if (gDesk.frame != nullptr) cairo_surface_destroy(gDesk.frame);
|
||||
gDesk.panel = gDesk.frame = nullptr;
|
||||
if (gDesk.texture != nullptr) SDL_DestroyTexture(gDesk.texture);
|
||||
if (gDesk.renderer != nullptr) SDL_DestroyRenderer(gDesk.renderer);
|
||||
if (gDesk.window != nullptr) SDL_DestroyWindow(gDesk.window);
|
||||
gDesk.texture = nullptr;
|
||||
gDesk.renderer = nullptr;
|
||||
gDesk.window = nullptr;
|
||||
SDL_Quit();
|
||||
std::fprintf(stderr, "[preview] window closed\n");
|
||||
}
|
||||
|
||||
VrEvents VrOverlay::pollEvents() {
|
||||
VrEvents events;
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SDL_QUIT:
|
||||
// Like the dashboard icon's "close": the panel quits
|
||||
events.closeRequested = true;
|
||||
break;
|
||||
case SDL_KEYDOWN:
|
||||
if (event.key.repeat == 0 && (event.key.keysym.sym == SDLK_SPACE || event.key.keysym.sym == SDLK_TAB ||
|
||||
event.key.keysym.sym == SDLK_ESCAPE)) {
|
||||
setDashboard(event.key.keysym.sym == SDLK_ESCAPE ? false : !gDesk.dashboardOpen, events);
|
||||
}
|
||||
break;
|
||||
case SDL_MOUSEMOTION: {
|
||||
const double x = event.motion.x, y = event.motion.y;
|
||||
const bool hover = inButton(x, y);
|
||||
if (hover != gDesk.buttonHover) {
|
||||
gDesk.buttonHover = hover;
|
||||
gDesk.dirty = true;
|
||||
}
|
||||
if (gDesk.dashboardOpen && y >= kBar) {
|
||||
events.pointer.push_back({PointerInput::Type::Move, x, y - kBar});
|
||||
gDesk.pointerInPanel = true;
|
||||
} else if (gDesk.pointerInPanel) {
|
||||
events.pointer.push_back({PointerInput::Type::Leave, 0, 0});
|
||||
gDesk.pointerInPanel = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
if (event.button.button != SDL_BUTTON_LEFT) break;
|
||||
if (inButton(event.button.x, event.button.y)) {
|
||||
setDashboard(!gDesk.dashboardOpen, events);
|
||||
} else if (gDesk.dashboardOpen && event.button.y >= kBar) {
|
||||
events.pointer.push_back({PointerInput::Type::Down, double(event.button.x), double(event.button.y - kBar)});
|
||||
} else if (!gDesk.dashboardOpen && event.button.y >= kBar) {
|
||||
// Clicking into the headset view opens the dashboard again, like the system button
|
||||
setDashboard(true, events);
|
||||
}
|
||||
break;
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
if (event.button.button == SDL_BUTTON_LEFT && gDesk.dashboardOpen && event.button.y >= kBar) {
|
||||
events.pointer.push_back({PointerInput::Type::Up, double(event.button.x), double(event.button.y - kBar)});
|
||||
}
|
||||
break;
|
||||
case SDL_WINDOWEVENT:
|
||||
if (event.window.event == SDL_WINDOWEVENT_LEAVE && gDesk.pointerInPanel) {
|
||||
events.pointer.push_back({PointerInput::Type::Leave, 0, 0});
|
||||
gDesk.pointerInPanel = false;
|
||||
}
|
||||
gDesk.dirty = true;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
runScript(events);
|
||||
// The headset view moves (the fit's dot, the gaze dots) without the panel changing, so it is redrawn
|
||||
// regularly; the panel view only when something changed.
|
||||
const Uint32 now = SDL_GetTicks();
|
||||
if (gDesk.dirty || (!gDesk.dashboardOpen && now - gDesk.lastDraw >= 11) || now - gDesk.lastDraw >= 500) redraw();
|
||||
return events;
|
||||
}
|
||||
|
||||
bool VrOverlay::steamVrAlive() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VrOverlay::panelVisible() const {
|
||||
return gDesk.dashboardOpen;
|
||||
}
|
||||
|
||||
void VrOverlay::showPanel() {
|
||||
VrEvents ignored;
|
||||
setDashboard(true, ignored);
|
||||
if (gDesk.window != nullptr) SDL_RaiseWindow(gDesk.window);
|
||||
}
|
||||
|
||||
bool VrOverlay::dashboardVisible() const {
|
||||
return gDesk.dashboardOpen;
|
||||
}
|
||||
|
||||
double VrOverlay::userIpdMeters() const {
|
||||
return gaze_fit::kDefaultIpdM;
|
||||
}
|
||||
|
||||
bool VrOverlay::showTarget(double yawDeg, double pitchDeg, const uint8_t* rgba, int size) {
|
||||
if (rgba != nullptr) gDesk.target.set(rgba, size);
|
||||
if (!gDesk.targetShown || yawDeg != gDesk.targetYaw || pitchDeg != gDesk.targetPitch || rgba != nullptr) gDesk.dirty = true;
|
||||
gDesk.targetShown = true;
|
||||
gDesk.targetYaw = yawDeg;
|
||||
gDesk.targetPitch = pitchDeg;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VrOverlay::waitFrameSync(uint32_t /*timeoutMs*/) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void VrOverlay::hideTarget() {
|
||||
if (gDesk.targetShown) gDesk.dirty = true;
|
||||
gDesk.targetShown = false;
|
||||
}
|
||||
|
||||
bool VrOverlay::showDot(int index, double x, double y, double z, double /*yawDeg*/, double /*pitchDeg*/, const uint8_t* rgba,
|
||||
int size, bool newImage, double widthM) {
|
||||
if (index < 0 || index > 1) return false;
|
||||
Dot& dot = gDesk.dots[index];
|
||||
if (newImage || dot.image.size == 0) dot.image.set(rgba, size);
|
||||
dot.shown = true;
|
||||
dot.x = x;
|
||||
dot.y = y;
|
||||
dot.z = z;
|
||||
dot.width = widthM;
|
||||
return true;
|
||||
}
|
||||
|
||||
void VrOverlay::hideDot(int index) {
|
||||
if (index >= 0 && index <= 1) gDesk.dots[index].shown = false;
|
||||
}
|
||||
|
||||
bool VrOverlay::submitThumbnail(const uint8_t* rgba, int size) {
|
||||
if (gDesk.window == nullptr) return false;
|
||||
// The dashboard thumbnail (the eye) becomes the window icon
|
||||
SDL_Surface* icon = SDL_CreateRGBSurfaceWithFormatFrom(const_cast<uint8_t*>(rgba), size, size, 32, size * 4,
|
||||
SDL_PIXELFORMAT_RGBA32);
|
||||
if (icon == nullptr) return false;
|
||||
SDL_SetWindowIcon(gDesk.window, icon);
|
||||
SDL_FreeSurface(icon);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VrOverlay::submitPanel(const uint8_t* rgba) {
|
||||
if (gDesk.panel != nullptr) cairo_surface_destroy(gDesk.panel);
|
||||
gDesk.panel = toSurface(rgba, gDesk.width, gDesk.panelHeight);
|
||||
gDesk.dirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void VrOverlay::logOverlayState(const char* when) const {
|
||||
std::fprintf(stderr, "[preview] panel shown in a desktop window (%s)\n", when);
|
||||
}
|
||||
|
||||
int VrOverlay::probe() {
|
||||
std::printf("--probe needs SteamVR; this is the desktop preview build\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int VrOverlay::switchAway(double /*seconds*/) {
|
||||
std::printf("--probe-switch-away needs SteamVR; this is the desktop preview build\n");
|
||||
return 1;
|
||||
}
|
||||
Reference in new issue
Block a user