GraphicApplication: Vulkan renderer, native windows, input and frame diagnostics #9
183 changed files with 12901 additions and 2133 deletions
|
|
@ -18,7 +18,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE ${BINDINGS_LIBS})
|
|||
file(GLOB APP_SOURCES "./applications/*.cpp")
|
||||
add_executable(3DEditorApp ${APP_SOURCES})
|
||||
target_link_libraries(3DEditorApp ${PROJECT_NAME})
|
||||
graphic_application_resources(3DEditorApp)
|
||||
|
||||
file(COPY "../RasterRender/rsc" DESTINATION "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/")
|
||||
file(COPY "../Graphics/rsc" DESTINATION "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/")
|
||||
file(COPY "../3DScene/rsc" DESTINATION "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/")
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ using namespace tp;
|
|||
class EditorGUI : public WidgetApplication {
|
||||
public:
|
||||
EditorGUI() {
|
||||
auto canvas = this->mGraphics->getCanvas();
|
||||
auto canvas = &this->canvas();
|
||||
mGui = new EditorWidget(canvas, &mEditor);
|
||||
|
||||
mEditor.loadDefaults();
|
||||
|
|
|
|||
|
|
@ -10,28 +10,28 @@ namespace tp {
|
|||
public:
|
||||
explicit ViewportWidget(Canvas* canvas, Editor* editor) {
|
||||
mEditor = editor;
|
||||
mImage = canvas->createImageFromTextId(mEditor->getViewportTexID(), { 0, 0 });
|
||||
mImage = canvas->createImageFromRenderTarget(mEditor->getViewportTexID());
|
||||
mCanvas = canvas;
|
||||
}
|
||||
|
||||
~ViewportWidget() override { mCanvas->deleteImageHandle(mImage); }
|
||||
~ViewportWidget() override { mCanvas->deleteImage(mImage); }
|
||||
|
||||
void process(const EventHandler& events) override {
|
||||
void process(const InputView& events) override {
|
||||
mEditor->setViewportSize(getAreaT().size);
|
||||
}
|
||||
|
||||
void draw(Canvas& canvas) override {
|
||||
mEditor->renderViewport();
|
||||
|
||||
canvas.updateTextureID(mImage, mEditor->getViewportTexID());
|
||||
canvas.drawImage(getArea().relative(), &mImage, PI);
|
||||
canvas.updateImageFromRenderTarget(mImage, mEditor->getViewportTexID());
|
||||
canvas.drawImage(getArea().relative(), mImage, PI);
|
||||
}
|
||||
|
||||
public:
|
||||
Editor* mEditor;
|
||||
|
||||
Canvas* mCanvas = nullptr;
|
||||
Canvas::ImageHandle mImage;
|
||||
ImageHandle mImage;
|
||||
};
|
||||
|
||||
class EditorWidget : public DockWidget {
|
||||
|
|
@ -91,7 +91,7 @@ namespace tp {
|
|||
mNavigationReset.setAction( [this]() { mEditor->navigationReset(); });
|
||||
}
|
||||
|
||||
void process(const EventHandler& events) override {
|
||||
void process(const InputView& events) override {
|
||||
DockWidget::process(events);
|
||||
|
||||
if (auto obj = mEditor->getActiveObject()) { // dummy rotate active object
|
||||
|
|
@ -107,7 +107,7 @@ namespace tp {
|
|||
auto pointerPrev = events.getPointerPrev();
|
||||
auto pointerRelative = (((pointer - activeArea.pos) / activeArea.size) - 0.5f) * 2;
|
||||
|
||||
if (activeArea.isInside(pointer) && events.isDown(InputID::MOUSE1)) {
|
||||
if (activeArea.isInside(pointer) && events.isDown(PointerButton::Left)) {
|
||||
switch (mNavigationType) {
|
||||
case ORBIT: mEditor->navigationOrbit(events.getPointerDelta() / activeArea.size * 3); break;
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ add_subdirectory(Allocators)
|
|||
add_subdirectory(Strings)
|
||||
add_subdirectory(Language)
|
||||
add_subdirectory(Connection)
|
||||
add_subdirectory(Graphics)
|
||||
add_subdirectory(GraphicApplication)
|
||||
add_subdirectory(DataAnalysis)
|
||||
add_subdirectory(Objects)
|
||||
add_subdirectory(Widgets)
|
||||
|
|
|
|||
4
GraphicApplication/.clang-format
Normal file
4
GraphicApplication/.clang-format
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
BasedOnStyle: InheritParentConfig
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
InsertBraces: true
|
||||
133
GraphicApplication/CMakeLists.txt
Normal file
133
GraphicApplication/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
project(GraphicApplication)
|
||||
|
||||
find_package(Vulkan REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
function(graphic_application_library TARGET_NAME LIBRARY_DIR)
|
||||
set(DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/${LIBRARY_DIR})
|
||||
file(GLOB SOURCES CONFIGURE_DEPENDS ${DIR}/private/*.cpp ${DIR}/private/*.hpp ${DIR}/public/*.hpp)
|
||||
add_library(${TARGET_NAME} STATIC ${SOURCES})
|
||||
if (EXISTS ${DIR}/public)
|
||||
target_include_directories(${TARGET_NAME} PUBLIC ${DIR}/public)
|
||||
endif ()
|
||||
target_include_directories(${TARGET_NAME} PRIVATE ${DIR}/private)
|
||||
endfunction()
|
||||
|
||||
### ---------------------- Input layer --------------------- ###
|
||||
graphic_application_library(${PROJECT_NAME}InputLayer input_layer)
|
||||
target_link_libraries(${PROJECT_NAME}InputLayer PUBLIC Math Modules)
|
||||
|
||||
### ---------------------- Diagnostics --------------------- ###
|
||||
graphic_application_library(${PROJECT_NAME}Diagnostics diagnostics)
|
||||
target_link_libraries(${PROJECT_NAME}Diagnostics PUBLIC Callstack Threads::Threads)
|
||||
|
||||
### ---------------------- GPU device and window surfaces --------------------- ###
|
||||
graphic_application_library(${PROJECT_NAME}Gpu gpu)
|
||||
target_link_libraries(${PROJECT_NAME}Gpu PUBLIC Vulkan::Vulkan PRIVATE ${PROJECT_NAME}Diagnostics)
|
||||
|
||||
### ---------------------- Wayland protocols --------------------- ###
|
||||
if (NOT ANDROID)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(WAYLAND REQUIRED IMPORTED_TARGET wayland-client xkbcommon)
|
||||
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
|
||||
pkg_get_variable(WAYLAND_SCANNER wayland-scanner wayland_scanner)
|
||||
|
||||
### ---------------------- Wayland Protocols --------------------- ###
|
||||
set(WAYLAND_PROTOCOL_FILES
|
||||
stable/xdg-shell/xdg-shell.xml
|
||||
stable/viewporter/viewporter.xml
|
||||
staging/fractional-scale/fractional-scale-v1.xml
|
||||
staging/cursor-shape/cursor-shape-v1.xml
|
||||
staging/xdg-activation/xdg-activation-v1.xml
|
||||
stable/tablet/tablet-v2.xml
|
||||
unstable/text-input/text-input-unstable-v3.xml
|
||||
unstable/xdg-decoration/xdg-decoration-unstable-v1.xml
|
||||
unstable/pointer-gestures/pointer-gestures-unstable-v1.xml
|
||||
)
|
||||
|
||||
set(WAYLAND_GENERATED_DIR ${CMAKE_CURRENT_BINARY_DIR}/wayland)
|
||||
file(MAKE_DIRECTORY ${WAYLAND_GENERATED_DIR})
|
||||
|
||||
foreach(PROTOCOL_FILE ${WAYLAND_PROTOCOL_FILES})
|
||||
get_filename_component(PROTOCOL_NAME ${PROTOCOL_FILE} NAME_WE)
|
||||
set(PROTOCOL_XML ${WAYLAND_PROTOCOLS_DIR}/${PROTOCOL_FILE})
|
||||
set(PROTOCOL_HEADER ${WAYLAND_GENERATED_DIR}/${PROTOCOL_NAME}-client-protocol.h)
|
||||
set(PROTOCOL_SOURCE ${WAYLAND_GENERATED_DIR}/${PROTOCOL_NAME}-protocol.c)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${PROTOCOL_HEADER} ${PROTOCOL_SOURCE}
|
||||
COMMAND ${WAYLAND_SCANNER} client-header ${PROTOCOL_XML} ${PROTOCOL_HEADER}
|
||||
COMMAND ${WAYLAND_SCANNER} private-code ${PROTOCOL_XML} ${PROTOCOL_SOURCE}
|
||||
DEPENDS ${PROTOCOL_XML}
|
||||
)
|
||||
list(APPEND WAYLAND_PROTOCOL_SOURCES ${PROTOCOL_HEADER} ${PROTOCOL_SOURCE})
|
||||
endforeach()
|
||||
|
||||
add_library(WaylandProtocols STATIC ${WAYLAND_PROTOCOL_SOURCES})
|
||||
target_include_directories(WaylandProtocols PUBLIC ${WAYLAND_GENERATED_DIR})
|
||||
target_link_libraries(WaylandProtocols PUBLIC PkgConfig::WAYLAND)
|
||||
set_target_properties(WaylandProtocols PROPERTIES LINKER_LANGUAGE C)
|
||||
endif ()
|
||||
|
||||
### ---------------------- Backend contract --------------------- ###
|
||||
graphic_application_library(${PROJECT_NAME}BackendContract backends/contract)
|
||||
target_link_libraries(${PROJECT_NAME}BackendContract PUBLIC Math ${PROJECT_NAME}InputLayer ${PROJECT_NAME}Gpu)
|
||||
|
||||
### ---------------------- Platform backend --------------------- ###
|
||||
if (ANDROID)
|
||||
graphic_application_library(${PROJECT_NAME}Backend backends/android)
|
||||
target_link_libraries(${PROJECT_NAME}Backend PUBLIC native_app_glue PRIVATE android log)
|
||||
else ()
|
||||
graphic_application_library(${PROJECT_NAME}Backend backends/wayland)
|
||||
target_link_libraries(${PROJECT_NAME}Backend PRIVATE WaylandProtocols PkgConfig::WAYLAND)
|
||||
endif ()
|
||||
target_link_libraries(${PROJECT_NAME}Backend PUBLIC ${PROJECT_NAME}BackendContract)
|
||||
|
||||
### ---------------------- Window --------------------- ###
|
||||
graphic_application_library(${PROJECT_NAME}Window window)
|
||||
target_link_libraries(${PROJECT_NAME}Window PUBLIC Math ${PROJECT_NAME}BackendContract PRIVATE ${PROJECT_NAME}Backend)
|
||||
|
||||
### ---------------------- Canvas --------------------- ###
|
||||
graphic_application_library(${PROJECT_NAME}Canvas canvas)
|
||||
target_link_libraries(${PROJECT_NAME}Canvas PUBLIC Math Containers ${PROJECT_NAME}BackendContract PRIVATE ${PROJECT_NAME}Diagnostics NanovgVk)
|
||||
|
||||
### ---------------------- Window decorations --------------------- ###
|
||||
graphic_application_library(${PROJECT_NAME}Decorations decorations)
|
||||
target_link_libraries(${PROJECT_NAME}Decorations PUBLIC ${PROJECT_NAME}Canvas)
|
||||
|
||||
### ---------------------- Debug UI --------------------- ###
|
||||
graphic_application_library(${PROJECT_NAME}DebugUI debug_ui)
|
||||
target_link_libraries(${PROJECT_NAME}DebugUI PUBLIC ${PROJECT_NAME}Window PRIVATE Imgui)
|
||||
|
||||
### ---------------------- Application --------------------- ###
|
||||
graphic_application_library(${PROJECT_NAME} application)
|
||||
target_link_libraries(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}Window ${PROJECT_NAME}Canvas ${PROJECT_NAME}Decorations ${PROJECT_NAME}DebugUI ${PROJECT_NAME}Diagnostics)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE MODULES_GENERIC_APP_ID="online.ilushabasic.modules" MODULES_GENERIC_APP_NAME="Modules")
|
||||
|
||||
# Copies the shared fonts into an "rsc" directory next to an executable, where the desktop backend looks for them.
|
||||
set(GRAPHIC_APPLICATION_RESOURCES ${CMAKE_CURRENT_SOURCE_DIR}/rsc CACHE INTERNAL "")
|
||||
function(graphic_application_resources TARGET_NAME)
|
||||
add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${GRAPHIC_APPLICATION_RESOURCES} $<TARGET_FILE_DIR:${TARGET_NAME}>/rsc
|
||||
)
|
||||
endfunction()
|
||||
|
||||
### -------------------------- Examples -------------------------- ###
|
||||
if (NOT ANDROID)
|
||||
add_executable(Example${PROJECT_NAME} ./examples/Example.cpp)
|
||||
target_link_libraries(Example${PROJECT_NAME} ${PROJECT_NAME} Imgui)
|
||||
graphic_application_resources(Example${PROJECT_NAME})
|
||||
|
||||
add_executable(OffscreenProof ./examples/OffscreenProof.cpp)
|
||||
target_link_libraries(OffscreenProof NanovgVk Vulkan::Vulkan)
|
||||
endif ()
|
||||
|
||||
### -------------------------- Tests -------------------------- ###
|
||||
if (NOT ANDROID)
|
||||
file(GLOB TEST_SOURCES CONFIGURE_DEPENDS "./tests/*.cpp")
|
||||
add_executable(Test${PROJECT_NAME} ${TEST_SOURCES})
|
||||
target_include_directories(Test${PROJECT_NAME} PRIVATE ./src/canvas/private ./src/backends/wayland/private)
|
||||
target_compile_definitions(Test${PROJECT_NAME} PRIVATE MODULES_SOURCE_DIR="${CMAKE_SOURCE_DIR}")
|
||||
target_link_libraries(Test${PROJECT_NAME} ${PROJECT_NAME}Window ${PROJECT_NAME}Backend ${PROJECT_NAME}Diagnostics NanovgVk UnitTest++)
|
||||
add_test(NAME Test${PROJECT_NAME} COMMAND Test${PROJECT_NAME})
|
||||
endif ()
|
||||
25
GraphicApplication/examples/Example.cpp
Normal file
25
GraphicApplication/examples/Example.cpp
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#include "GraphicApplication.hpp"
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
using namespace tp;
|
||||
|
||||
class ExampleDelegate : public ApplicationDelegate {
|
||||
public:
|
||||
void attach(Application& application) { mApplication = &application; }
|
||||
|
||||
void drawFrame(Canvas&) override {
|
||||
ImGui::ShowDemoWindow();
|
||||
mApplication->showFrameRates();
|
||||
}
|
||||
|
||||
private:
|
||||
Application* mApplication = nullptr;
|
||||
};
|
||||
|
||||
int main() {
|
||||
ExampleDelegate delegate;
|
||||
Application application(delegate);
|
||||
delegate.attach(application);
|
||||
application.run();
|
||||
}
|
||||
378
GraphicApplication/examples/OffscreenProof.cpp
Normal file
378
GraphicApplication/examples/OffscreenProof.cpp
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include <nanovg.h>
|
||||
#include <nanovg_vk.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kWidth = 256;
|
||||
constexpr uint32_t kHeight = 256;
|
||||
constexpr VkFormat kColorFormat = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
constexpr VkFormat kDepthFormat = VK_FORMAT_D24_UNORM_S8_UINT;
|
||||
|
||||
void check(VkResult result, const char* what) {
|
||||
if (result != VK_SUCCESS) {
|
||||
throw std::runtime_error(std::string(what) + " failed");
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t findMemoryType(VkPhysicalDevice gpu, uint32_t filter, VkMemoryPropertyFlags flags) {
|
||||
VkPhysicalDeviceMemoryProperties properties{};
|
||||
vkGetPhysicalDeviceMemoryProperties(gpu, &properties);
|
||||
for (uint32_t i = 0; i < properties.memoryTypeCount; i++) {
|
||||
if ((filter & (1 << i)) && (properties.memoryTypes[i].propertyFlags & flags) == flags) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
throw std::runtime_error("no memory type");
|
||||
}
|
||||
|
||||
struct Image {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
Image createImage(
|
||||
VkPhysicalDevice gpu, VkDevice device, VkFormat format, VkImageUsageFlags usage, VkImageAspectFlags aspect
|
||||
) {
|
||||
Image out;
|
||||
VkImageCreateInfo info{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
|
||||
.imageType = VK_IMAGE_TYPE_2D,
|
||||
.format = format,
|
||||
.extent = { kWidth, kHeight, 1 },
|
||||
.mipLevels = 1,
|
||||
.arrayLayers = 1,
|
||||
.samples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.tiling = VK_IMAGE_TILING_OPTIMAL,
|
||||
.usage = usage,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
};
|
||||
check(vkCreateImage(device, &info, nullptr, &out.image), "vkCreateImage");
|
||||
|
||||
VkMemoryRequirements req{};
|
||||
vkGetImageMemoryRequirements(device, out.image, &req);
|
||||
VkMemoryAllocateInfo alloc{
|
||||
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
|
||||
.allocationSize = req.size,
|
||||
.memoryTypeIndex = findMemoryType(gpu, req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT),
|
||||
};
|
||||
check(vkAllocateMemory(device, &alloc, nullptr, &out.memory), "vkAllocateMemory");
|
||||
vkBindImageMemory(device, out.image, out.memory, 0);
|
||||
|
||||
VkImageViewCreateInfo viewInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||
.image = out.image,
|
||||
.viewType = VK_IMAGE_VIEW_TYPE_2D,
|
||||
.format = format,
|
||||
.subresourceRange = { aspect, 0, 1, 0, 1 },
|
||||
};
|
||||
check(vkCreateImageView(device, &viewInfo, nullptr, &out.view), "vkCreateImageView");
|
||||
return out;
|
||||
}
|
||||
|
||||
void writePpm(const char* path, const std::vector<uint8_t>& rgba) {
|
||||
FILE* file = std::fopen(path, "wb");
|
||||
std::fprintf(file, "P6\n%u %u\n255\n", kWidth, kHeight);
|
||||
for (uint32_t i = 0; i < kWidth * kHeight; i++) {
|
||||
std::fputc(rgba[i * 4 + 0], file);
|
||||
std::fputc(rgba[i * 4 + 1], file);
|
||||
std::fputc(rgba[i * 4 + 2], file);
|
||||
}
|
||||
std::fclose(file);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
VkApplicationInfo appInfo{ .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .apiVersion = VK_API_VERSION_1_2 };
|
||||
VkInstanceCreateInfo instanceInfo{ .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pApplicationInfo = &appInfo };
|
||||
VkInstance instance = VK_NULL_HANDLE;
|
||||
check(vkCreateInstance(&instanceInfo, nullptr, &instance), "vkCreateInstance");
|
||||
|
||||
uint32_t gpuCount = 0;
|
||||
vkEnumeratePhysicalDevices(instance, &gpuCount, nullptr);
|
||||
std::vector<VkPhysicalDevice> gpus(gpuCount);
|
||||
vkEnumeratePhysicalDevices(instance, &gpuCount, gpus.data());
|
||||
VkPhysicalDevice gpu = gpus.at(0);
|
||||
|
||||
uint32_t familyCount = 0;
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(gpu, &familyCount, nullptr);
|
||||
std::vector<VkQueueFamilyProperties> families(familyCount);
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(gpu, &familyCount, families.data());
|
||||
uint32_t graphicsFamily = 0;
|
||||
for (uint32_t i = 0; i < familyCount; i++) {
|
||||
if (families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
|
||||
graphicsFamily = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t deviceExtCount = 0;
|
||||
vkEnumerateDeviceExtensionProperties(gpu, nullptr, &deviceExtCount, nullptr);
|
||||
std::vector<VkExtensionProperties> deviceExts(deviceExtCount);
|
||||
vkEnumerateDeviceExtensionProperties(gpu, nullptr, &deviceExtCount, deviceExts.data());
|
||||
bool hasExtendedDynamicState = false;
|
||||
for (const auto& extension : deviceExts) {
|
||||
if (std::strcmp(extension.extensionName, VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME) == 0) {
|
||||
hasExtendedDynamicState = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<const char*> enabledDeviceExts;
|
||||
if (hasExtendedDynamicState) {
|
||||
enabledDeviceExts.push_back(VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME);
|
||||
}
|
||||
|
||||
VkPhysicalDeviceExtendedDynamicStateFeaturesEXT dynamicStateFeatures{
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT,
|
||||
.extendedDynamicState = VK_TRUE,
|
||||
};
|
||||
|
||||
const float priority = 1.0f;
|
||||
VkDeviceQueueCreateInfo queueInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
|
||||
.queueFamilyIndex = graphicsFamily,
|
||||
.queueCount = 1,
|
||||
.pQueuePriorities = &priority,
|
||||
};
|
||||
VkDeviceCreateInfo deviceInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
|
||||
.pNext = hasExtendedDynamicState ? &dynamicStateFeatures : nullptr,
|
||||
.queueCreateInfoCount = 1,
|
||||
.pQueueCreateInfos = &queueInfo,
|
||||
.enabledExtensionCount = static_cast<uint32_t>(enabledDeviceExts.size()),
|
||||
.ppEnabledExtensionNames = enabledDeviceExts.data(),
|
||||
};
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
check(vkCreateDevice(gpu, &deviceInfo, nullptr, &device), "vkCreateDevice");
|
||||
std::printf("extended_dynamic_state: %s\n", hasExtendedDynamicState ? "enabled" : "unavailable");
|
||||
|
||||
VkQueue queue = VK_NULL_HANDLE;
|
||||
vkGetDeviceQueue(device, graphicsFamily, 0, &queue);
|
||||
|
||||
Image color = createImage(
|
||||
gpu,
|
||||
device,
|
||||
kColorFormat,
|
||||
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||
VK_IMAGE_ASPECT_COLOR_BIT
|
||||
);
|
||||
Image depth = createImage(
|
||||
gpu,
|
||||
device,
|
||||
kDepthFormat,
|
||||
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
|
||||
VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT
|
||||
);
|
||||
|
||||
const std::array<VkAttachmentDescription, 2> attachments = {
|
||||
VkAttachmentDescription{
|
||||
.format = kColorFormat,
|
||||
.samples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
|
||||
.storeOp = VK_ATTACHMENT_STORE_OP_STORE,
|
||||
.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
|
||||
.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
},
|
||||
VkAttachmentDescription{
|
||||
.format = kDepthFormat,
|
||||
.samples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
|
||||
.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
|
||||
.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
|
||||
.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
|
||||
},
|
||||
};
|
||||
VkAttachmentReference colorRef{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
|
||||
VkAttachmentReference depthRef{ 1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL };
|
||||
VkSubpassDescription subpass{
|
||||
.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
.colorAttachmentCount = 1,
|
||||
.pColorAttachments = &colorRef,
|
||||
.pDepthStencilAttachment = &depthRef,
|
||||
};
|
||||
VkRenderPassCreateInfo renderPassInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
|
||||
.attachmentCount = static_cast<uint32_t>(attachments.size()),
|
||||
.pAttachments = attachments.data(),
|
||||
.subpassCount = 1,
|
||||
.pSubpasses = &subpass,
|
||||
};
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
check(vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass), "vkCreateRenderPass");
|
||||
|
||||
const std::array<VkImageView, 2> framebufferViews = { color.view, depth.view };
|
||||
VkFramebufferCreateInfo framebufferInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
|
||||
.renderPass = renderPass,
|
||||
.attachmentCount = static_cast<uint32_t>(framebufferViews.size()),
|
||||
.pAttachments = framebufferViews.data(),
|
||||
.width = kWidth,
|
||||
.height = kHeight,
|
||||
.layers = 1,
|
||||
};
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
check(vkCreateFramebuffer(device, &framebufferInfo, nullptr, &framebuffer), "vkCreateFramebuffer");
|
||||
|
||||
VkCommandPoolCreateInfo poolInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
|
||||
.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
|
||||
.queueFamilyIndex = graphicsFamily,
|
||||
};
|
||||
VkCommandPool pool = VK_NULL_HANDLE;
|
||||
check(vkCreateCommandPool(device, &poolInfo, nullptr, &pool), "vkCreateCommandPool");
|
||||
|
||||
std::array<VkCommandBuffer, 1> commandBuffers{};
|
||||
VkCommandBufferAllocateInfo cmdAlloc{
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
|
||||
.commandPool = pool,
|
||||
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
|
||||
.commandBufferCount = 1,
|
||||
};
|
||||
check(vkAllocateCommandBuffers(device, &cmdAlloc, commandBuffers.data()), "vkAllocateCommandBuffers");
|
||||
|
||||
uint32_t currentFrame = 0;
|
||||
VKNVGCreateInfo nvgInfo{};
|
||||
nvgInfo.gpu = gpu;
|
||||
nvgInfo.device = device;
|
||||
nvgInfo.renderpass = renderPass;
|
||||
nvgInfo.cmdBuffer = commandBuffers.data();
|
||||
nvgInfo.graphicsQueueFamilyIndex = graphicsFamily;
|
||||
nvgInfo.currentFrame = ¤tFrame;
|
||||
nvgInfo.swapchainImageCount = 1;
|
||||
nvgInfo.ext.dynamicState = hasExtendedDynamicState;
|
||||
NVGcontext* vg = nvgCreateVk(&nvgInfo, NVG_ANTIALIAS | NVG_STENCIL_STROKES, queue);
|
||||
if (vg == nullptr) {
|
||||
throw std::runtime_error("nvgCreateVk returned null");
|
||||
}
|
||||
|
||||
VkCommandBuffer cmd = commandBuffers[0];
|
||||
VkCommandBufferBeginInfo begin{ .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
|
||||
vkBeginCommandBuffer(cmd, &begin);
|
||||
|
||||
std::array<VkClearValue, 2> clears{};
|
||||
clears[0].color = { { 0.10f, 0.10f, 0.12f, 1.0f } };
|
||||
clears[1].depthStencil = { 1.0f, 0 };
|
||||
VkRenderPassBeginInfo pass{
|
||||
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
|
||||
.renderPass = renderPass,
|
||||
.framebuffer = framebuffer,
|
||||
.renderArea = { { 0, 0 }, { kWidth, kHeight } },
|
||||
.clearValueCount = static_cast<uint32_t>(clears.size()),
|
||||
.pClearValues = clears.data(),
|
||||
};
|
||||
vkCmdBeginRenderPass(cmd, &pass, VK_SUBPASS_CONTENTS_INLINE);
|
||||
|
||||
VkViewport viewport{
|
||||
.x = 0.0f,
|
||||
.y = 0.0f,
|
||||
.width = static_cast<float>(kWidth),
|
||||
.height = static_cast<float>(kHeight),
|
||||
.minDepth = 0.0f,
|
||||
.maxDepth = 1.0f,
|
||||
};
|
||||
vkCmdSetViewport(cmd, 0, 1, &viewport);
|
||||
VkRect2D scissor{ { 0, 0 }, { kWidth, kHeight } };
|
||||
vkCmdSetScissor(cmd, 0, 1, &scissor);
|
||||
|
||||
nvgBeginFrame(vg, kWidth, kHeight, 1.0f);
|
||||
|
||||
nvgBeginPath(vg);
|
||||
nvgRect(vg, 40, 40, 120, 80);
|
||||
nvgFillColor(vg, nvgRGBA(220, 80, 60, 255));
|
||||
nvgFill(vg);
|
||||
|
||||
nvgBeginPath(vg);
|
||||
nvgCircle(vg, 170, 170, 50);
|
||||
nvgFillColor(vg, nvgRGBA(60, 160, 230, 255));
|
||||
nvgFill(vg);
|
||||
|
||||
nvgBeginPath(vg);
|
||||
nvgMoveTo(vg, 30, 220);
|
||||
nvgLineTo(vg, 230, 150);
|
||||
nvgStrokeColor(vg, nvgRGBA(240, 220, 40, 255));
|
||||
nvgStrokeWidth(vg, 6);
|
||||
nvgStroke(vg);
|
||||
|
||||
nvgEndFrame(vg);
|
||||
|
||||
vkCmdEndRenderPass(cmd);
|
||||
vkEndCommandBuffer(cmd);
|
||||
|
||||
VkSubmitInfo submit{
|
||||
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
|
||||
.commandBufferCount = 1,
|
||||
.pCommandBuffers = &cmd,
|
||||
};
|
||||
vkQueueSubmit(queue, 1, &submit, VK_NULL_HANDLE);
|
||||
vkQueueWaitIdle(queue);
|
||||
|
||||
VkBufferCreateInfo bufferInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
|
||||
.size = static_cast<VkDeviceSize>(kWidth) * kHeight * 4,
|
||||
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
||||
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
|
||||
};
|
||||
VkBuffer readback = VK_NULL_HANDLE;
|
||||
check(vkCreateBuffer(device, &bufferInfo, nullptr, &readback), "vkCreateBuffer");
|
||||
VkMemoryRequirements bufReq{};
|
||||
vkGetBufferMemoryRequirements(device, readback, &bufReq);
|
||||
VkMemoryAllocateInfo bufAlloc{
|
||||
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
|
||||
.allocationSize = bufReq.size,
|
||||
.memoryTypeIndex = findMemoryType(
|
||||
gpu, bufReq.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
|
||||
),
|
||||
};
|
||||
VkDeviceMemory readbackMemory = VK_NULL_HANDLE;
|
||||
check(vkAllocateMemory(device, &bufAlloc, nullptr, &readbackMemory), "vkAllocateMemory readback");
|
||||
vkBindBufferMemory(device, readback, readbackMemory, 0);
|
||||
|
||||
vkResetCommandBuffer(cmd, 0);
|
||||
vkBeginCommandBuffer(cmd, &begin);
|
||||
VkBufferImageCopy region{
|
||||
.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
|
||||
.imageExtent = { kWidth, kHeight, 1 },
|
||||
};
|
||||
vkCmdCopyImageToBuffer(cmd, color.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback, 1, ®ion);
|
||||
vkEndCommandBuffer(cmd);
|
||||
vkQueueSubmit(queue, 1, &submit, VK_NULL_HANDLE);
|
||||
vkQueueWaitIdle(queue);
|
||||
|
||||
void* mapped = nullptr;
|
||||
vkMapMemory(device, readbackMemory, 0, bufferInfo.size, 0, &mapped);
|
||||
std::vector<uint8_t> pixels(bufferInfo.size);
|
||||
std::memcpy(pixels.data(), mapped, bufferInfo.size);
|
||||
vkUnmapMemory(device, readbackMemory);
|
||||
|
||||
writePpm("nanovg_offscreen.ppm", pixels);
|
||||
|
||||
uint64_t nonBackground = 0;
|
||||
for (uint32_t i = 0; i < kWidth * kHeight; i++) {
|
||||
const uint8_t r = pixels[i * 4 + 0];
|
||||
const uint8_t g = pixels[i * 4 + 1];
|
||||
const uint8_t b = pixels[i * 4 + 2];
|
||||
if (r > 40 || g > 40 || b > 40) {
|
||||
nonBackground++;
|
||||
}
|
||||
}
|
||||
std::printf(
|
||||
"rendered %llu / %u non-background pixels\n", static_cast<unsigned long long>(nonBackground), kWidth * kHeight
|
||||
);
|
||||
|
||||
nvgDeleteVk(vg);
|
||||
return nonBackground > 1000 ? 0 : 1;
|
||||
}
|
||||
BIN
GraphicApplication/rsc/Emoji.ttf
Normal file
BIN
GraphicApplication/rsc/Emoji.ttf
Normal file
Binary file not shown.
56
GraphicApplication/rsc/FONTS.md
Normal file
56
GraphicApplication/rsc/FONTS.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Bundled fonts
|
||||
|
||||
`GraphicApplication/private/drawing/CanvasFonts.hpp` names these files, `Canvas` loads them
|
||||
relative to the application's `rsc/` directory, and `GraphicApplication/tests/FontCoverage`
|
||||
checks that every text face can draw the characters the UI uses and the Russian
|
||||
alphabet. All of them are SIL Open Font License (see `licenses/`).
|
||||
|
||||
| File | Family | Version | upem | Glyphs | Role |
|
||||
|---|---|---|---|---|---|
|
||||
| `Font.ttf` | Cantarell Regular | 1.004 | 2048 | 388 | Regular text face |
|
||||
| `FontBold.ttf` | Cantarell Bold | 1.004 | 2048 | 388 | Bold text face |
|
||||
| `FontMono.ttf` | JetBrains Mono | 2.304 | 1000 | 1363 | Monospace text face, code |
|
||||
| `FontCyrillic.ttf` | Cantarell Cyrillic Regular | 0.111 | 1000 | 182 | Cyrillic fallback of the regular face |
|
||||
| `FontCyrillicBold.ttf` | Cantarell Cyrillic Bold | 0.111 | 1000 | 182 | Cyrillic fallback of the bold face |
|
||||
| `FontSymbols.ttf` | Noto Sans Symbols 2 | 2.008 | 1000 | 637 | Arrows, shapes, dingbats |
|
||||
| `FontMath.ttf` | Noto Sans Math | 3.000 | 1000 | 904 | Mathematical operators |
|
||||
| `FontSpecials.ttf` | Noto Sans | 2.015 | 1000 | 1 | U+FFFD replacement character |
|
||||
| `Emoji.ttf` | Noto Emoji | 1.05 | 2048 | 887 | Emoji |
|
||||
|
||||
Every text face falls back to the symbol fonts and then to a Cyrillic cut of
|
||||
Cantarell. Cantarell itself has no Cyrillic in the 1.004 static release the
|
||||
faces use (the Google Fonts build), and Russian task titles, descriptions and
|
||||
Jira imports are data, so they used to render as empty boxes. Cantarell 0.111
|
||||
does have Cyrillic, so the cuts come from it; its upem and advances differ from
|
||||
1.004, which does not matter because the two are only mixed inside a fallback
|
||||
chain and nanovg scales each font by its own upem. Vertical metrics agree
|
||||
(x-height 0.482 em, cap height 0.694 em), so the two generations sit on one
|
||||
line.
|
||||
|
||||
`android/assets/rsc` symlinks these files for the Android app; add a symlink
|
||||
there when you add a font.
|
||||
|
||||
## Regenerating the Cyrillic cuts
|
||||
|
||||
Sources: Cantarell 0.111 `Cantarell-Regular.otf` / `Cantarell-Bold.otf`
|
||||
(https://download.gnome.org/sources/cantarell/0.111/).
|
||||
|
||||
```sh
|
||||
# 1. Keep the Cyrillic blocks and №, drop the layout features we do not use.
|
||||
pyftsubset Cantarell-Regular.otf \
|
||||
--unicodes="U+0400-04FF,U+0500-052F,U+1C80-1C88,U+2116" \
|
||||
--output-file=Cantarell-Cyrillic.otf --layout-features='' --no-hinting --desubroutinize
|
||||
|
||||
# 2. Cantarell 0.111 is CFF ("Cantarell-Regular.otf"); stb_truetype, which
|
||||
# nanovg uses, needs TrueType outlines, so convert with FontForge.
|
||||
fontforge -lang=py -c "import fontforge; f = fontforge.open('Cantarell-Cyrillic.otf'); f.generate('FontCyrillic.ttf')"
|
||||
|
||||
# 3. Rename it (family "Cantarell Cyrillic", PostScript name
|
||||
# "Cantarell-Cyrillic-Regular") and set OS/2 usWeightClass and head.macStyle
|
||||
# to 400/700 with fontTools, so the file cannot be confused with the full
|
||||
# Cantarell and the bold cut is recognisably bold.
|
||||
```
|
||||
|
||||
Checking the result: glyph bounding boxes must match the source OTF exactly
|
||||
(scale by upem), and `stbtt_InitFont` must accept the file and find every
|
||||
Cyrillic codepoint — `TestGraphicApplication` fails loudly otherwise.
|
||||
BIN
GraphicApplication/rsc/FontBold.ttf
Normal file
BIN
GraphicApplication/rsc/FontBold.ttf
Normal file
Binary file not shown.
BIN
GraphicApplication/rsc/FontCyrillic.ttf
Normal file
BIN
GraphicApplication/rsc/FontCyrillic.ttf
Normal file
Binary file not shown.
BIN
GraphicApplication/rsc/FontCyrillicBold.ttf
Normal file
BIN
GraphicApplication/rsc/FontCyrillicBold.ttf
Normal file
Binary file not shown.
BIN
GraphicApplication/rsc/FontMath.ttf
Normal file
BIN
GraphicApplication/rsc/FontMath.ttf
Normal file
Binary file not shown.
BIN
GraphicApplication/rsc/FontMono.ttf
Normal file
BIN
GraphicApplication/rsc/FontMono.ttf
Normal file
Binary file not shown.
BIN
GraphicApplication/rsc/FontSpecials.ttf
Normal file
BIN
GraphicApplication/rsc/FontSpecials.ttf
Normal file
Binary file not shown.
BIN
GraphicApplication/rsc/FontSymbols.ttf
Normal file
BIN
GraphicApplication/rsc/FontSymbols.ttf
Normal file
Binary file not shown.
93
GraphicApplication/rsc/licenses/Cantarell-OFL.txt
Normal file
93
GraphicApplication/rsc/licenses/Cantarell-OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2009 The Cantarell Project Authors (https://github.com/davelab6/cantarell)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
93
GraphicApplication/rsc/licenses/JetBrainsMono-OFL.txt
Normal file
93
GraphicApplication/rsc/licenses/JetBrainsMono-OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
93
GraphicApplication/rsc/licenses/Noto-OFL.txt
Normal file
93
GraphicApplication/rsc/licenses/Noto-OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/symbols, https://github.com/notofonts/math, https://github.com/notofonts/latin-greek-cyrillic)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
#include "GraphicApplication.hpp"
|
||||
|
||||
using namespace tp;
|
||||
|
||||
WindowIdentity WindowIdentity::generic() { return { MODULES_GENERIC_APP_ID, MODULES_GENERIC_APP_NAME }; }
|
||||
|
||||
Application::Application(ApplicationDelegate& delegate, const ApplicationOptions& options) :
|
||||
mDelegate(delegate),
|
||||
mProcessSection(mProfiler.section("frame.process")),
|
||||
mPresentSection(mProfiler.section("frame.present")),
|
||||
mDrawSection(mProfiler.section("frame.draw")),
|
||||
mInputEventsCounter(mProfiler.counter("input.events")),
|
||||
mGpu(Window::createGpuDevice()),
|
||||
mWindow(std::make_unique<Window>(*mGpu, mProfiler, options.window)),
|
||||
mCanvas(std::make_unique<Canvas>(mWindow->surface(), mProfiler, options.resourceDirectory)),
|
||||
mDebugGUI(std::make_unique<DebugGUI>(*mWindow, options.resourceDirectory, options.debugInterface)) {
|
||||
mHangSettings.application = HangWatchdog::processName();
|
||||
mPerSecondTimer.setDuration(1000.f);
|
||||
}
|
||||
|
||||
Application::~Application() { finish(); }
|
||||
|
||||
void Application::setHangSettings(HangSettings settings) { mHangSettings = std::move(settings); }
|
||||
|
||||
void Application::start() {
|
||||
if (mRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
mHangWatchdog = std::make_unique<HangWatchdog>(mHangSettings, mProfiler.trail());
|
||||
for (const HangWatchdog::ReportFileSource& reportFileSource : mHangReportFileSources) {
|
||||
mHangWatchdog->addReportFileSource(reportFileSource);
|
||||
}
|
||||
|
||||
mWindow->loop().waitForReady();
|
||||
|
||||
mPreviousFrameTime = get_time();
|
||||
mPerSecondTimer.reset();
|
||||
mRunning = true;
|
||||
runFrame();
|
||||
}
|
||||
|
||||
void Application::step(halnf deltaMs) {
|
||||
if (!mRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
enterPhase(UiPhase::Idle);
|
||||
mWindow->loop().processEvents(false);
|
||||
runFrame(deltaMs);
|
||||
}
|
||||
|
||||
void Application::finish() {
|
||||
if (!mRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
mHangWatchdog.reset();
|
||||
mRunning = false;
|
||||
}
|
||||
|
||||
void Application::run() {
|
||||
start();
|
||||
|
||||
WindowEventLoop& loop = mWindow->loop();
|
||||
while (mRunning && !mWindow->controls().shouldClose()) {
|
||||
enterPhase(UiPhase::Idle);
|
||||
loop.processEvents(!wantsFrame() || loop.isWaitingForFrame());
|
||||
while (loop.isWaitingForFrame() && !mWindow->controls().shouldClose()) {
|
||||
loop.processEvents(true);
|
||||
}
|
||||
runFrame();
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
void Application::addHangReportFileSource(HangWatchdog::ReportFileSource reportFileSource) {
|
||||
if (mHangWatchdog) {
|
||||
mHangWatchdog->addReportFileSource(reportFileSource);
|
||||
}
|
||||
mHangReportFileSources.push_back(std::move(reportFileSource));
|
||||
}
|
||||
|
||||
void Application::enterPhase(UiPhase phase) {
|
||||
if (mHangWatchdog) {
|
||||
mHangWatchdog->enterPhase(phase);
|
||||
}
|
||||
}
|
||||
|
||||
bool Application::wantsFrame() { return mDelegate.wantsFrame() || mWindow->loop().hasPendingWork(); }
|
||||
|
||||
void Application::runFrame() {
|
||||
updateGlobalTime();
|
||||
|
||||
const time_ms now = get_time();
|
||||
const halnf deltaMs = halnf(now - mPreviousFrameTime);
|
||||
mPreviousFrameTime = now;
|
||||
runFrame(deltaMs);
|
||||
}
|
||||
|
||||
void Application::runFrame(halnf deltaMs) {
|
||||
mProfiler.beginFrame();
|
||||
|
||||
enterPhase(UiPhase::Input);
|
||||
processPendingInput(deltaMs);
|
||||
|
||||
enterPhase(UiPhase::Draw);
|
||||
present();
|
||||
updateFrameCounters();
|
||||
|
||||
mProfiler.endFrame();
|
||||
}
|
||||
|
||||
void Application::processPendingInput(halnf deltaMs) {
|
||||
const ProfileScope scope(mProfiler, mProcessSection);
|
||||
|
||||
const size_t eventCount = mWindow->loop().advanceFrame();
|
||||
mProfiler.addCounter(mInputEventsCounter, (double) eventCount);
|
||||
if (mHangWatchdog) {
|
||||
mHangWatchdog->noteInputEvents(eventCount);
|
||||
}
|
||||
|
||||
mDelegate.processFrame(mWindow->input().state(), deltaMs);
|
||||
mFramesProcessed++;
|
||||
}
|
||||
|
||||
void Application::present() {
|
||||
const ProfileScope scope(mProfiler, mPresentSection);
|
||||
const std::optional<GpuFrame> frame = mWindow->loop().frameBegin();
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
|
||||
mCanvas->drawBegin(*frame, mWindow->frame());
|
||||
mDebugGUI->drawBegin();
|
||||
|
||||
drawFrameContent();
|
||||
|
||||
enterPhase(UiPhase::Present);
|
||||
mCanvas->drawEnd();
|
||||
mDebugGUI->drawEnd(*frame);
|
||||
|
||||
mWindow->loop().frameEnd();
|
||||
mFramesDrawn++;
|
||||
}
|
||||
|
||||
void Application::drawFrameContent() {
|
||||
const ProfileScope scope(mProfiler, mDrawSection);
|
||||
const WindowFrame& frame = mWindow->frame();
|
||||
|
||||
mDecorations.paintBase(*mCanvas, frame);
|
||||
{
|
||||
const Canvas::SavedState saved(*mCanvas);
|
||||
mDelegate.drawFrame(*mCanvas);
|
||||
}
|
||||
mDecorations.paintFrame(*mCanvas, frame, mWindow->controls().isFocused());
|
||||
}
|
||||
|
||||
void Application::updateFrameCounters() {
|
||||
if (!mPerSecondTimer.isTimeout()) {
|
||||
return;
|
||||
}
|
||||
mPerSecondTimer.reset();
|
||||
|
||||
mFramesProcessedPerSecond = mFramesProcessed;
|
||||
mFramesDrawnPerSecond = mFramesDrawn;
|
||||
mFramesDrawn = 0;
|
||||
mFramesProcessed = 0;
|
||||
}
|
||||
|
||||
void Application::showFrameRates() {
|
||||
mDebugGUI->showValue("Frames processed per second", mFramesProcessedPerSecond);
|
||||
mDebugGUI->showValue("Frames drawn per second", mFramesDrawnPerSecond);
|
||||
}
|
||||
175
GraphicApplication/src/application/private/SessionRecording.cpp
Normal file
175
GraphicApplication/src/application/private/SessionRecording.cpp
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
#include "SessionRecording.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
double steadyMs() {
|
||||
const auto now = std::chrono::steady_clock::now().time_since_epoch();
|
||||
return std::chrono::duration<double, std::milli>(now).count();
|
||||
}
|
||||
|
||||
bool writeText(const std::filesystem::path& path, const std::string& text) {
|
||||
std::ofstream file(path, std::ios::binary | std::ios::trunc);
|
||||
file.write(text.data(), (std::streamsize) text.size());
|
||||
return (bool) file;
|
||||
}
|
||||
}
|
||||
|
||||
std::string SessionEnvironment::serialize() const {
|
||||
char size[64];
|
||||
std::snprintf(size, sizeof(size), "%.1f %.1f", windowSize.x, windowSize.y);
|
||||
char scale[32];
|
||||
std::snprintf(scale, sizeof(scale), "%.3f", contentScale);
|
||||
return "application " + application + "\nrevision " + revision + "\ndevice " + device + "\ndate " + date +
|
||||
"\nwindow " + size + "\nscale " + scale + "\n";
|
||||
}
|
||||
|
||||
bool SessionRecording::save(const std::string& directory) const {
|
||||
const std::filesystem::path root(directory);
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(root, error);
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool written = writeText(root / "environment.txt", environment.serialize());
|
||||
written = written && input.save((root / "input.txt").string());
|
||||
if (!recordedFrames.empty()) {
|
||||
written = written && writeText(root / "recorded-frames.tsv", recordedFrames);
|
||||
}
|
||||
if (saveSnapshot) {
|
||||
written = written && saveSnapshot(directory);
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
struct SessionRecorder::Recording {
|
||||
explicit Recording(InputRecorder::Clock clock) :
|
||||
input(std::move(clock)) {}
|
||||
|
||||
SessionEnvironment environment;
|
||||
SessionRecording::Saver snapshot;
|
||||
InputRecorder input;
|
||||
double startMs = 0;
|
||||
bool profilerWasEnabled = false;
|
||||
};
|
||||
|
||||
SessionRecorder::SessionRecorder(Application& application, Clock clock) :
|
||||
mApplication(application),
|
||||
mClock(clock ? std::move(clock) : Clock(steadyMs)) {}
|
||||
|
||||
SessionRecorder::~SessionRecorder() { cancel(); }
|
||||
|
||||
double SessionRecorder::elapsedMs() const { return mRecording ? mClock() - mRecording->startMs : 0.0; }
|
||||
|
||||
SessionEnvironment SessionRecorder::environment() const {
|
||||
SessionEnvironment environment;
|
||||
environment.application = HangWatchdog::processName();
|
||||
environment.revision = mRevision;
|
||||
environment.device = mApplication.gpu().deviceName();
|
||||
|
||||
const std::time_t now = std::time(nullptr);
|
||||
char date[64];
|
||||
std::strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S", std::localtime(&now));
|
||||
environment.date = date;
|
||||
|
||||
environment.windowSize = mApplication.window().contentSize();
|
||||
environment.contentScale = mApplication.window().contentScale();
|
||||
return environment;
|
||||
}
|
||||
|
||||
void SessionRecorder::start() {
|
||||
if (mRecording) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SessionEnvironment recorded = environment();
|
||||
mRecording = std::make_unique<Recording>(mClock);
|
||||
mRecording->startMs = mClock();
|
||||
mRecording->environment = recorded;
|
||||
mRecording->snapshot = mSnapshot ? mSnapshot(recorded) : SessionRecording::Saver();
|
||||
mRecording->input.setWindowSize(recorded.windowSize);
|
||||
mRecording->input.startClockNow();
|
||||
|
||||
Profiler& profiler = mApplication.profiler();
|
||||
mRecording->profilerWasEnabled = profiler.isEnabled();
|
||||
profiler.clear();
|
||||
profiler.setEnabled(true);
|
||||
mApplication.window().input().setRawInputTap(&mRecording->input);
|
||||
}
|
||||
|
||||
SessionRecording SessionRecorder::stop() {
|
||||
SessionRecording session;
|
||||
if (!mRecording) {
|
||||
return session;
|
||||
}
|
||||
|
||||
mApplication.window().input().setRawInputTap(nullptr);
|
||||
session.environment = std::move(mRecording->environment);
|
||||
session.input = mRecording->input.recording();
|
||||
session.recordedFrames = recordedFramesTsv(mApplication.profiler());
|
||||
session.saveSnapshot = std::move(mRecording->snapshot);
|
||||
restoreProfiler();
|
||||
return session;
|
||||
}
|
||||
|
||||
void SessionRecorder::cancel() {
|
||||
if (!mRecording) {
|
||||
return;
|
||||
}
|
||||
mApplication.window().input().setRawInputTap(nullptr);
|
||||
restoreProfiler();
|
||||
}
|
||||
|
||||
void SessionRecorder::restoreProfiler() {
|
||||
Profiler& profiler = mApplication.profiler();
|
||||
profiler.setEnabled(mRecording->profilerWasEnabled);
|
||||
profiler.clear();
|
||||
mRecording.reset();
|
||||
}
|
||||
|
||||
std::string SessionRecorder::defaultDirectory(const std::string& applicationSessionsDirectory) {
|
||||
const char* configured = std::getenv("MODULES_SESSION_DIR");
|
||||
if (configured && *configured) {
|
||||
return configured;
|
||||
}
|
||||
return applicationSessionsDirectory;
|
||||
}
|
||||
|
||||
std::string SessionRecorder::recordedFramesTsv(const Profiler& profiler) {
|
||||
std::string text = "frame\twall_ms\tcpu_ms";
|
||||
for (const std::string& name : profiler.sectionNames()) {
|
||||
text += "\t" + name;
|
||||
}
|
||||
for (const std::string& name : profiler.counterNames()) {
|
||||
text += "\t" + name;
|
||||
}
|
||||
text += "\n";
|
||||
|
||||
char value[32];
|
||||
auto append = [&](double number) {
|
||||
std::snprintf(value, sizeof(value), "\t%.4f", number);
|
||||
text += value;
|
||||
};
|
||||
for (const FrameProfile& frame : profiler.frames()) {
|
||||
text += std::to_string(frame.index);
|
||||
append(frame.wallMs);
|
||||
append(frame.cpuMs);
|
||||
for (size_t index = 0; index < profiler.sectionNames().size(); index++) {
|
||||
append(index < frame.sectionMs.size() ? frame.sectionMs[index] : 0.0);
|
||||
}
|
||||
for (size_t index = 0; index < profiler.counterNames().size(); index++) {
|
||||
append(index < frame.counters.size() ? frame.counters[index] : 0.0);
|
||||
}
|
||||
text += "\n";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
#pragma once
|
||||
|
||||
#include "Canvas.hpp"
|
||||
#include "DebugGUI.hpp"
|
||||
#include "HangWatchdog.hpp"
|
||||
#include "Profiler.hpp"
|
||||
#include "Timing.hpp"
|
||||
#include "Window.hpp"
|
||||
#include "WindowDecorations.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
struct ApplicationOptions {
|
||||
WindowOptions window = { .identity = WindowIdentity::generic() };
|
||||
std::filesystem::path resourceDirectory = Window::platformResourceDirectory();
|
||||
DebugInterface debugInterface = DebugInterface::Visible;
|
||||
};
|
||||
|
||||
// What an application does each frame. Application drives the frame loop and calls it.
|
||||
class ApplicationDelegate {
|
||||
public:
|
||||
virtual ~ApplicationDelegate() = default;
|
||||
|
||||
[[nodiscard]] virtual bool wantsFrame() { return false; }
|
||||
virtual void processFrame(const InputState& input, halnf deltaMs) {}
|
||||
virtual void drawFrame(Canvas& canvas) {}
|
||||
};
|
||||
|
||||
class Application {
|
||||
public:
|
||||
explicit Application(ApplicationDelegate& delegate, const ApplicationOptions& options = {});
|
||||
~Application();
|
||||
|
||||
Application(const Application&) = delete;
|
||||
Application& operator=(const Application&) = delete;
|
||||
|
||||
void run();
|
||||
void start();
|
||||
void step(halnf deltaMs);
|
||||
void finish();
|
||||
[[nodiscard]] bool isRunning() const { return mRunning; }
|
||||
|
||||
[[nodiscard]] Window& window() { return *mWindow; }
|
||||
[[nodiscard]] Canvas& canvas() { return *mCanvas; }
|
||||
[[nodiscard]] DebugGUI& debugGUI() { return *mDebugGUI; }
|
||||
[[nodiscard]] Profiler& profiler() { return mProfiler; }
|
||||
[[nodiscard]] GpuDevice& gpu() { return *mGpu; }
|
||||
|
||||
void setHangSettings(HangSettings settings);
|
||||
void addHangReportFileSource(HangWatchdog::ReportFileSource reportFileSource);
|
||||
[[nodiscard]] HangWatchdog* hangWatchdog() { return mHangWatchdog.get(); }
|
||||
|
||||
[[nodiscard]] halnf framesProcessedPerSecond() const { return mFramesProcessedPerSecond; }
|
||||
[[nodiscard]] halnf framesDrawnPerSecond() const { return mFramesDrawnPerSecond; }
|
||||
void showFrameRates();
|
||||
|
||||
private:
|
||||
void enterPhase(UiPhase phase);
|
||||
[[nodiscard]] bool wantsFrame();
|
||||
void runFrame();
|
||||
void runFrame(halnf deltaMs);
|
||||
void processPendingInput(halnf deltaMs);
|
||||
void present();
|
||||
void drawFrameContent();
|
||||
void updateFrameCounters();
|
||||
|
||||
ApplicationDelegate& mDelegate;
|
||||
Profiler mProfiler;
|
||||
HangSettings mHangSettings;
|
||||
|
||||
bool mRunning = false;
|
||||
time_ms mPreviousFrameTime = 0;
|
||||
std::unique_ptr<HangWatchdog> mHangWatchdog;
|
||||
std::vector<HangWatchdog::ReportFileSource> mHangReportFileSources;
|
||||
|
||||
Timer mPerSecondTimer;
|
||||
halnf mFramesProcessedPerSecond = 0;
|
||||
halnf mFramesDrawnPerSecond = 0;
|
||||
halnf mFramesProcessed = 0;
|
||||
halnf mFramesDrawn = 0;
|
||||
|
||||
ProfileId mProcessSection;
|
||||
ProfileId mPresentSection;
|
||||
ProfileId mDrawSection;
|
||||
ProfileId mInputEventsCounter;
|
||||
|
||||
WindowDecorations mDecorations;
|
||||
std::unique_ptr<GpuDevice> mGpu;
|
||||
std::unique_ptr<Window> mWindow;
|
||||
std::unique_ptr<Canvas> mCanvas;
|
||||
std::unique_ptr<DebugGUI> mDebugGUI;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
#pragma once
|
||||
|
||||
#include "GraphicApplication.hpp"
|
||||
#include "InputRecording.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace tp {
|
||||
|
||||
struct SessionEnvironment {
|
||||
std::string revision;
|
||||
std::string device;
|
||||
std::string date;
|
||||
Vec2F windowSize = { 0, 0 };
|
||||
halnf contentScale = 1;
|
||||
std::string application;
|
||||
|
||||
[[nodiscard]] std::string serialize() const;
|
||||
};
|
||||
|
||||
// A recorded session: the environment, every raw input event and the profiled frames, saved as one directory.
|
||||
struct SessionRecording {
|
||||
using Saver = std::function<bool(const std::string& directory)>;
|
||||
|
||||
SessionEnvironment environment;
|
||||
InputRecording input;
|
||||
std::string recordedFrames;
|
||||
Saver saveSnapshot;
|
||||
|
||||
bool save(const std::string& directory) const;
|
||||
};
|
||||
|
||||
// Records an application's raw input and frame profile between start() and stop(), for bug reports and replays.
|
||||
class SessionRecorder {
|
||||
public:
|
||||
using Snapshot = std::function<SessionRecording::Saver(const SessionEnvironment& environment)>;
|
||||
using Clock = std::function<double()>;
|
||||
|
||||
explicit SessionRecorder(Application& application, Clock clock = {});
|
||||
~SessionRecorder();
|
||||
|
||||
SessionRecorder(const SessionRecorder&) = delete;
|
||||
SessionRecorder& operator=(const SessionRecorder&) = delete;
|
||||
|
||||
void setSnapshot(Snapshot snapshot) { mSnapshot = std::move(snapshot); }
|
||||
void setRevision(std::string revision) { mRevision = std::move(revision); }
|
||||
|
||||
[[nodiscard]] bool isRecording() const { return mRecording != nullptr; }
|
||||
[[nodiscard]] double elapsedMs() const;
|
||||
|
||||
void start();
|
||||
[[nodiscard]] SessionRecording stop();
|
||||
void cancel();
|
||||
|
||||
[[nodiscard]] SessionEnvironment environment() const;
|
||||
|
||||
[[nodiscard]] static std::string defaultDirectory(const std::string& applicationSessionsDirectory);
|
||||
[[nodiscard]] static std::string recordedFramesTsv(const Profiler& profiler);
|
||||
|
||||
private:
|
||||
struct Recording;
|
||||
|
||||
void restoreProfiler();
|
||||
|
||||
Application& mApplication;
|
||||
Clock mClock;
|
||||
std::string mRevision = "dev";
|
||||
Snapshot mSnapshot;
|
||||
std::unique_ptr<Recording> mRecording;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
#include "AndroidEntry.hpp"
|
||||
|
||||
#include "AndroidPlatform.hpp"
|
||||
|
||||
#include <android/log.h>
|
||||
#include <android/native_activity.h>
|
||||
#include <android_native_app_glue.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
void drainUntilDestroyed(android_app* app) {
|
||||
while (!app->destroyRequested) {
|
||||
int events = 0;
|
||||
android_poll_source* source = nullptr;
|
||||
if (ALooper_pollOnce(-1, nullptr, &events, (void**) &source) >= 0 && source) {
|
||||
source->process(app, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void runAndroidApplication(
|
||||
android_app* app, AndroidMain main, const char* const* assetDirectories, int assetDirectoryCount
|
||||
) {
|
||||
for (int index = 0; index < assetDirectoryCount; index++) {
|
||||
android::extractAssets(*app, assetDirectories[index]);
|
||||
}
|
||||
setenv("HOME", app->activity->internalDataPath, 1);
|
||||
|
||||
try {
|
||||
main(app);
|
||||
} catch (const std::exception& error) {
|
||||
__android_log_print(ANDROID_LOG_FATAL, "modules", "application failed: %s", error.what());
|
||||
}
|
||||
|
||||
if (!app->destroyRequested) {
|
||||
ANativeActivity_finish(app->activity);
|
||||
}
|
||||
drainUntilDestroyed(app);
|
||||
}
|
||||
}
|
||||
130
GraphicApplication/src/backends/android/private/AndroidKeys.cpp
Normal file
130
GraphicApplication/src/backends/android/private/AndroidKeys.cpp
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
#include "AndroidKeys.hpp"
|
||||
|
||||
#include <android/keycodes.h>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
namespace tp::android {
|
||||
|
||||
namespace {
|
||||
const std::unordered_map<int32_t, Key>& keyCodes() {
|
||||
static const std::unordered_map<int32_t, Key> keys = {
|
||||
{ AKEYCODE_SPACE, Key::Space },
|
||||
{ AKEYCODE_APOSTROPHE, Key::Apostrophe },
|
||||
{ AKEYCODE_COMMA, Key::Comma },
|
||||
{ AKEYCODE_MINUS, Key::Minus },
|
||||
{ AKEYCODE_PERIOD, Key::Period },
|
||||
{ AKEYCODE_SLASH, Key::Slash },
|
||||
{ AKEYCODE_0, Key::Digit0 },
|
||||
{ AKEYCODE_1, Key::Digit1 },
|
||||
{ AKEYCODE_2, Key::Digit2 },
|
||||
{ AKEYCODE_3, Key::Digit3 },
|
||||
{ AKEYCODE_4, Key::Digit4 },
|
||||
{ AKEYCODE_5, Key::Digit5 },
|
||||
{ AKEYCODE_6, Key::Digit6 },
|
||||
{ AKEYCODE_7, Key::Digit7 },
|
||||
{ AKEYCODE_8, Key::Digit8 },
|
||||
{ AKEYCODE_9, Key::Digit9 },
|
||||
{ AKEYCODE_SEMICOLON, Key::Semicolon },
|
||||
{ AKEYCODE_EQUALS, Key::Equal },
|
||||
{ AKEYCODE_A, Key::A },
|
||||
{ AKEYCODE_B, Key::B },
|
||||
{ AKEYCODE_C, Key::C },
|
||||
{ AKEYCODE_D, Key::D },
|
||||
{ AKEYCODE_E, Key::E },
|
||||
{ AKEYCODE_F, Key::F },
|
||||
{ AKEYCODE_G, Key::G },
|
||||
{ AKEYCODE_H, Key::H },
|
||||
{ AKEYCODE_I, Key::I },
|
||||
{ AKEYCODE_J, Key::J },
|
||||
{ AKEYCODE_K, Key::K },
|
||||
{ AKEYCODE_L, Key::L },
|
||||
{ AKEYCODE_M, Key::M },
|
||||
{ AKEYCODE_N, Key::N },
|
||||
{ AKEYCODE_O, Key::O },
|
||||
{ AKEYCODE_P, Key::P },
|
||||
{ AKEYCODE_Q, Key::Q },
|
||||
{ AKEYCODE_R, Key::R },
|
||||
{ AKEYCODE_S, Key::S },
|
||||
{ AKEYCODE_T, Key::T },
|
||||
{ AKEYCODE_U, Key::U },
|
||||
{ AKEYCODE_V, Key::V },
|
||||
{ AKEYCODE_W, Key::W },
|
||||
{ AKEYCODE_X, Key::X },
|
||||
{ AKEYCODE_Y, Key::Y },
|
||||
{ AKEYCODE_Z, Key::Z },
|
||||
{ AKEYCODE_LEFT_BRACKET, Key::LeftBracket },
|
||||
{ AKEYCODE_BACKSLASH, Key::Backslash },
|
||||
{ AKEYCODE_RIGHT_BRACKET, Key::RightBracket },
|
||||
{ AKEYCODE_GRAVE, Key::GraveAccent },
|
||||
{ AKEYCODE_ESCAPE, Key::Escape },
|
||||
{ AKEYCODE_ENTER, Key::Enter },
|
||||
{ AKEYCODE_TAB, Key::Tab },
|
||||
{ AKEYCODE_DEL, Key::Backspace },
|
||||
{ AKEYCODE_INSERT, Key::Insert },
|
||||
{ AKEYCODE_FORWARD_DEL, Key::Delete },
|
||||
{ AKEYCODE_DPAD_RIGHT, Key::Right },
|
||||
{ AKEYCODE_DPAD_LEFT, Key::Left },
|
||||
{ AKEYCODE_DPAD_DOWN, Key::Down },
|
||||
{ AKEYCODE_DPAD_UP, Key::Up },
|
||||
{ AKEYCODE_PAGE_UP, Key::PageUp },
|
||||
{ AKEYCODE_PAGE_DOWN, Key::PageDown },
|
||||
{ AKEYCODE_MOVE_HOME, Key::Home },
|
||||
{ AKEYCODE_MOVE_END, Key::End },
|
||||
{ AKEYCODE_CAPS_LOCK, Key::CapsLock },
|
||||
{ AKEYCODE_SCROLL_LOCK, Key::ScrollLock },
|
||||
{ AKEYCODE_NUM_LOCK, Key::NumLock },
|
||||
{ AKEYCODE_SYSRQ, Key::PrintScreen },
|
||||
{ AKEYCODE_BREAK, Key::Pause },
|
||||
{ AKEYCODE_F1, Key::F1 },
|
||||
{ AKEYCODE_F2, Key::F2 },
|
||||
{ AKEYCODE_F3, Key::F3 },
|
||||
{ AKEYCODE_F4, Key::F4 },
|
||||
{ AKEYCODE_F5, Key::F5 },
|
||||
{ AKEYCODE_F6, Key::F6 },
|
||||
{ AKEYCODE_F7, Key::F7 },
|
||||
{ AKEYCODE_F8, Key::F8 },
|
||||
{ AKEYCODE_F9, Key::F9 },
|
||||
{ AKEYCODE_F10, Key::F10 },
|
||||
{ AKEYCODE_F11, Key::F11 },
|
||||
{ AKEYCODE_F12, Key::F12 },
|
||||
{ AKEYCODE_NUMPAD_0, Key::Keypad0 },
|
||||
{ AKEYCODE_NUMPAD_1, Key::Keypad1 },
|
||||
{ AKEYCODE_NUMPAD_2, Key::Keypad2 },
|
||||
{ AKEYCODE_NUMPAD_3, Key::Keypad3 },
|
||||
{ AKEYCODE_NUMPAD_4, Key::Keypad4 },
|
||||
{ AKEYCODE_NUMPAD_5, Key::Keypad5 },
|
||||
{ AKEYCODE_NUMPAD_6, Key::Keypad6 },
|
||||
{ AKEYCODE_NUMPAD_7, Key::Keypad7 },
|
||||
{ AKEYCODE_NUMPAD_8, Key::Keypad8 },
|
||||
{ AKEYCODE_NUMPAD_9, Key::Keypad9 },
|
||||
{ AKEYCODE_NUMPAD_DOT, Key::KeypadDecimal },
|
||||
{ AKEYCODE_NUMPAD_DIVIDE, Key::KeypadDivide },
|
||||
{ AKEYCODE_NUMPAD_MULTIPLY, Key::KeypadMultiply },
|
||||
{ AKEYCODE_NUMPAD_SUBTRACT, Key::KeypadSubtract },
|
||||
{ AKEYCODE_NUMPAD_ADD, Key::KeypadAdd },
|
||||
{ AKEYCODE_NUMPAD_ENTER, Key::KeypadEnter },
|
||||
{ AKEYCODE_NUMPAD_EQUALS, Key::KeypadEqual },
|
||||
{ AKEYCODE_SHIFT_LEFT, Key::LeftShift },
|
||||
{ AKEYCODE_CTRL_LEFT, Key::LeftControl },
|
||||
{ AKEYCODE_ALT_LEFT, Key::LeftAlt },
|
||||
{ AKEYCODE_META_LEFT, Key::LeftSuper },
|
||||
{ AKEYCODE_SHIFT_RIGHT, Key::RightShift },
|
||||
{ AKEYCODE_CTRL_RIGHT, Key::RightControl },
|
||||
{ AKEYCODE_ALT_RIGHT, Key::RightAlt },
|
||||
{ AKEYCODE_META_RIGHT, Key::RightSuper },
|
||||
{ AKEYCODE_MENU, Key::Menu },
|
||||
};
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Key> keyFromAndroid(int32_t keyCode) {
|
||||
const auto& keys = keyCodes();
|
||||
const auto found = keys.find(keyCode);
|
||||
if (found == keys.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return found->second;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputCodes.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace tp::android {
|
||||
|
||||
[[nodiscard]] std::optional<Key> keyFromAndroid(int32_t keyCode);
|
||||
}
|
||||
|
|
@ -0,0 +1,301 @@
|
|||
#include "AndroidPlatform.hpp"
|
||||
|
||||
#include <android/api-level.h>
|
||||
#include <android/asset_manager.h>
|
||||
#include <android/input.h>
|
||||
#include <android/log.h>
|
||||
#include <android/native_activity.h>
|
||||
#include <android_native_app_glue.h>
|
||||
#include <jni.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
|
||||
namespace tp::android {
|
||||
|
||||
namespace {
|
||||
|
||||
struct AttachedEnv {
|
||||
explicit AttachedEnv(ANativeActivity* activity) :
|
||||
vm(activity->vm) {
|
||||
if (vm->GetEnv((void**) &env, JNI_VERSION_1_6) == JNI_EDETACHED) {
|
||||
vm->AttachCurrentThread(&env, nullptr);
|
||||
attached = true;
|
||||
}
|
||||
}
|
||||
|
||||
~AttachedEnv() {
|
||||
if (attached) {
|
||||
vm->DetachCurrentThread();
|
||||
}
|
||||
}
|
||||
|
||||
JavaVM* vm;
|
||||
JNIEnv* env = nullptr;
|
||||
bool attached = false;
|
||||
};
|
||||
|
||||
void appendUtf8(std::string& out, uint32_t codepoint) {
|
||||
if (codepoint < 0x80) {
|
||||
out += (char) codepoint;
|
||||
} else if (codepoint < 0x800) {
|
||||
out += (char) (0xC0 | (codepoint >> 6));
|
||||
out += (char) (0x80 | (codepoint & 0x3F));
|
||||
} else if (codepoint < 0x10000) {
|
||||
out += (char) (0xE0 | (codepoint >> 12));
|
||||
out += (char) (0x80 | ((codepoint >> 6) & 0x3F));
|
||||
out += (char) (0x80 | (codepoint & 0x3F));
|
||||
} else {
|
||||
out += (char) (0xF0 | (codepoint >> 18));
|
||||
out += (char) (0x80 | ((codepoint >> 12) & 0x3F));
|
||||
out += (char) (0x80 | ((codepoint >> 6) & 0x3F));
|
||||
out += (char) (0x80 | (codepoint & 0x3F));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string unicodeText(android_app& app, const AInputEvent* event) {
|
||||
AttachedEnv attached(app.activity);
|
||||
JNIEnv* env = attached.env;
|
||||
if (!env) {
|
||||
return {};
|
||||
}
|
||||
|
||||
jclass keyEventClass = env->FindClass("android/view/KeyEvent");
|
||||
jmethodID constructor = env->GetMethodID(keyEventClass, "<init>", "(JJIIIIIIII)V");
|
||||
jmethodID getUnicodeChar = env->GetMethodID(keyEventClass, "getUnicodeChar", "(I)I");
|
||||
jobject keyEvent = env->NewObject(
|
||||
keyEventClass,
|
||||
constructor,
|
||||
(jlong) AKeyEvent_getDownTime(event),
|
||||
(jlong) AKeyEvent_getEventTime(event),
|
||||
(jint) AKeyEvent_getAction(event),
|
||||
(jint) AKeyEvent_getKeyCode(event),
|
||||
(jint) AKeyEvent_getRepeatCount(event),
|
||||
(jint) AKeyEvent_getMetaState(event),
|
||||
(jint) AInputEvent_getDeviceId(event),
|
||||
(jint) AKeyEvent_getScanCode(event),
|
||||
(jint) AKeyEvent_getFlags(event),
|
||||
(jint) AInputEvent_getSource(event)
|
||||
);
|
||||
const jint codepoint = env->CallIntMethod(keyEvent, getUnicodeChar, (jint) AKeyEvent_getMetaState(event));
|
||||
env->DeleteLocalRef(keyEvent);
|
||||
env->DeleteLocalRef(keyEventClass);
|
||||
|
||||
std::string text;
|
||||
if (codepoint > 0 && codepoint >= 0x20 && codepoint != 0x7F) {
|
||||
appendUtf8(text, (uint32_t) codepoint);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
void setSoftKeyboardVisible(android_app& app, bool visible) {
|
||||
AttachedEnv attached(app.activity);
|
||||
JNIEnv* env = attached.env;
|
||||
if (!env) {
|
||||
return;
|
||||
}
|
||||
|
||||
jobject activity = app.activity->clazz;
|
||||
jclass activityClass = env->GetObjectClass(activity);
|
||||
jclass contextClass = env->FindClass("android/content/Context");
|
||||
jfieldID serviceField = env->GetStaticFieldID(contextClass, "INPUT_METHOD_SERVICE", "Ljava/lang/String;");
|
||||
jobject serviceName = env->GetStaticObjectField(contextClass, serviceField);
|
||||
jmethodID getSystemService =
|
||||
env->GetMethodID(activityClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
|
||||
jobject inputManager = env->CallObjectMethod(activity, getSystemService, serviceName);
|
||||
|
||||
jmethodID getWindow = env->GetMethodID(activityClass, "getWindow", "()Landroid/view/Window;");
|
||||
jobject window = env->CallObjectMethod(activity, getWindow);
|
||||
jclass windowClass = env->FindClass("android/view/Window");
|
||||
jmethodID getDecorView = env->GetMethodID(windowClass, "getDecorView", "()Landroid/view/View;");
|
||||
jobject decorView = env->CallObjectMethod(window, getDecorView);
|
||||
|
||||
jclass managerClass = env->FindClass("android/view/inputmethod/InputMethodManager");
|
||||
if (visible) {
|
||||
jmethodID show = env->GetMethodID(managerClass, "showSoftInput", "(Landroid/view/View;I)Z");
|
||||
env->CallBooleanMethod(inputManager, show, decorView, 0);
|
||||
} else {
|
||||
jclass viewClass = env->FindClass("android/view/View");
|
||||
jmethodID getToken = env->GetMethodID(viewClass, "getWindowToken", "()Landroid/os/IBinder;");
|
||||
jobject token = env->CallObjectMethod(decorView, getToken);
|
||||
jmethodID hide = env->GetMethodID(managerClass, "hideSoftInputFromWindow", "(Landroid/os/IBinder;I)Z");
|
||||
env->CallBooleanMethod(inputManager, hide, token, 0);
|
||||
env->DeleteLocalRef(token);
|
||||
env->DeleteLocalRef(viewClass);
|
||||
}
|
||||
if (env->ExceptionCheck()) {
|
||||
env->ExceptionClear();
|
||||
}
|
||||
|
||||
env->DeleteLocalRef(managerClass);
|
||||
env->DeleteLocalRef(decorView);
|
||||
env->DeleteLocalRef(windowClass);
|
||||
env->DeleteLocalRef(window);
|
||||
env->DeleteLocalRef(inputManager);
|
||||
env->DeleteLocalRef(serviceName);
|
||||
env->DeleteLocalRef(contextClass);
|
||||
env->DeleteLocalRef(activityClass);
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct InsetEdges {
|
||||
jobject bars = nullptr;
|
||||
jobject gestures = nullptr;
|
||||
};
|
||||
|
||||
InsetEdges typedInsetEdges(JNIEnv* env, jobject insets, jclass insetsClass) {
|
||||
jclass typeClass = env->FindClass("android/view/WindowInsets$Type");
|
||||
const auto typeMask = [&](const char* name) {
|
||||
return env->CallStaticIntMethod(typeClass, env->GetStaticMethodID(typeClass, name, "()I"));
|
||||
};
|
||||
const jint bars = typeMask("systemBars") | typeMask("displayCutout");
|
||||
const jint gestures = typeMask("mandatorySystemGestures");
|
||||
env->DeleteLocalRef(typeClass);
|
||||
|
||||
jmethodID getInsetsOf = env->GetMethodID(insetsClass, "getInsets", "(I)Landroid/graphics/Insets;");
|
||||
return { env->CallObjectMethod(insets, getInsetsOf, bars), env->CallObjectMethod(insets, getInsetsOf, gestures) };
|
||||
}
|
||||
|
||||
InsetEdges legacyInsetEdges(JNIEnv* env, jobject insets, jclass insetsClass) {
|
||||
const auto edgesOf = [&](const char* getter) {
|
||||
return env->CallObjectMethod(insets, env->GetMethodID(insetsClass, getter, "()Landroid/graphics/Insets;"));
|
||||
};
|
||||
return { edgesOf("getSystemWindowInsets"), edgesOf("getMandatorySystemGestureInsets") };
|
||||
}
|
||||
}
|
||||
|
||||
EdgeInsets systemInsets(android_app& app) {
|
||||
AttachedEnv attached(app.activity);
|
||||
JNIEnv* env = attached.env;
|
||||
if (!env) {
|
||||
return {};
|
||||
}
|
||||
|
||||
EdgeInsets result;
|
||||
jobject activity = app.activity->clazz;
|
||||
jclass activityClass = env->GetObjectClass(activity);
|
||||
jmethodID getWindow = env->GetMethodID(activityClass, "getWindow", "()Landroid/view/Window;");
|
||||
jobject window = env->CallObjectMethod(activity, getWindow);
|
||||
jclass windowClass = env->FindClass("android/view/Window");
|
||||
jmethodID getDecorView = env->GetMethodID(windowClass, "getDecorView", "()Landroid/view/View;");
|
||||
jobject decorView = window ? env->CallObjectMethod(window, getDecorView) : nullptr;
|
||||
jclass viewClass = env->FindClass("android/view/View");
|
||||
jmethodID getInsets = env->GetMethodID(viewClass, "getRootWindowInsets", "()Landroid/view/WindowInsets;");
|
||||
jobject insets = decorView ? env->CallObjectMethod(decorView, getInsets) : nullptr;
|
||||
|
||||
if (insets) {
|
||||
jclass insetsClass = env->FindClass("android/view/WindowInsets");
|
||||
const InsetEdges sources = android_get_device_api_level() >= 30 ? typedInsetEdges(env, insets, insetsClass)
|
||||
: legacyInsetEdges(env, insets, insetsClass);
|
||||
jclass edgesClass = env->FindClass("android/graphics/Insets");
|
||||
const auto edge = [&](jobject edges, const char* name) {
|
||||
return edges ? (halnf) env->GetIntField(edges, env->GetFieldID(edgesClass, name, "I")) : 0.0f;
|
||||
};
|
||||
const auto both = [&](const char* name) {
|
||||
return std::max(edge(sources.bars, name), edge(sources.gestures, name));
|
||||
};
|
||||
result = { both("left"), both("top"), both("right"), both("bottom") };
|
||||
|
||||
env->DeleteLocalRef(edgesClass);
|
||||
if (sources.gestures) {
|
||||
env->DeleteLocalRef(sources.gestures);
|
||||
}
|
||||
if (sources.bars) {
|
||||
env->DeleteLocalRef(sources.bars);
|
||||
}
|
||||
env->DeleteLocalRef(insetsClass);
|
||||
env->DeleteLocalRef(insets);
|
||||
}
|
||||
if (env->ExceptionCheck()) {
|
||||
env->ExceptionClear();
|
||||
}
|
||||
|
||||
env->DeleteLocalRef(viewClass);
|
||||
if (decorView) {
|
||||
env->DeleteLocalRef(decorView);
|
||||
}
|
||||
env->DeleteLocalRef(windowClass);
|
||||
if (window) {
|
||||
env->DeleteLocalRef(window);
|
||||
}
|
||||
env->DeleteLocalRef(activityClass);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool openUrl(android_app& app, const std::string& url) {
|
||||
AttachedEnv attached(app.activity);
|
||||
JNIEnv* env = attached.env;
|
||||
if (!env || url.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
jclass uriClass = env->FindClass("android/net/Uri");
|
||||
jmethodID parse = env->GetStaticMethodID(uriClass, "parse", "(Ljava/lang/String;)Landroid/net/Uri;");
|
||||
jstring text = env->NewStringUTF(url.c_str());
|
||||
jobject uri = env->CallStaticObjectMethod(uriClass, parse, text);
|
||||
|
||||
jclass intentClass = env->FindClass("android/content/Intent");
|
||||
jmethodID constructor = env->GetMethodID(intentClass, "<init>", "(Ljava/lang/String;Landroid/net/Uri;)V");
|
||||
jstring action = env->NewStringUTF("android.intent.action.VIEW");
|
||||
jobject intent = env->NewObject(intentClass, constructor, action, uri);
|
||||
jmethodID addFlags = env->GetMethodID(intentClass, "addFlags", "(I)Landroid/content/Intent;");
|
||||
constexpr jint kNewTask = 0x10000000;
|
||||
env->DeleteLocalRef(env->CallObjectMethod(intent, addFlags, kNewTask));
|
||||
|
||||
jobject activity = app.activity->clazz;
|
||||
jclass activityClass = env->GetObjectClass(activity);
|
||||
jmethodID startActivity = env->GetMethodID(activityClass, "startActivity", "(Landroid/content/Intent;)V");
|
||||
env->CallVoidMethod(activity, startActivity, intent);
|
||||
const bool failed = env->ExceptionCheck();
|
||||
if (failed) {
|
||||
env->ExceptionClear();
|
||||
}
|
||||
|
||||
env->DeleteLocalRef(activityClass);
|
||||
env->DeleteLocalRef(intent);
|
||||
env->DeleteLocalRef(action);
|
||||
env->DeleteLocalRef(intentClass);
|
||||
if (uri) {
|
||||
env->DeleteLocalRef(uri);
|
||||
}
|
||||
env->DeleteLocalRef(text);
|
||||
env->DeleteLocalRef(uriClass);
|
||||
return !failed;
|
||||
}
|
||||
|
||||
namespace {
|
||||
void
|
||||
extractDirectory(AAssetManager* manager, const std::string& assetDirectory, const std::filesystem::path& target) {
|
||||
AAssetDir* directory = AAssetManager_openDir(manager, assetDirectory.c_str());
|
||||
if (!directory) {
|
||||
return;
|
||||
}
|
||||
std::filesystem::create_directories(target);
|
||||
while (const char* name = AAssetDir_getNextFileName(directory)) {
|
||||
const std::string assetPath = assetDirectory.empty() ? name : assetDirectory + "/" + name;
|
||||
AAsset* asset = AAssetManager_open(manager, assetPath.c_str(), AASSET_MODE_STREAMING);
|
||||
if (!asset) {
|
||||
continue;
|
||||
}
|
||||
std::ofstream out(target / name, std::ios::binary | std::ios::trunc);
|
||||
std::vector<char> buffer(64 * 1024);
|
||||
int read = 0;
|
||||
while ((read = AAsset_read(asset, buffer.data(), buffer.size())) > 0) {
|
||||
out.write(buffer.data(), read);
|
||||
}
|
||||
AAsset_close(asset);
|
||||
}
|
||||
AAssetDir_close(directory);
|
||||
}
|
||||
}
|
||||
|
||||
void extractAssets(android_app& app, const char* directory) {
|
||||
AAssetManager* manager = app.activity->assetManager;
|
||||
const std::filesystem::path target = std::filesystem::path(app.activity->internalDataPath) / directory;
|
||||
extractDirectory(manager, directory, target);
|
||||
__android_log_print(ANDROID_LOG_INFO, "modules", "assets extracted to %s", target.c_str());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
#pragma once
|
||||
|
||||
#include "PlatformInfo.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
struct android_app;
|
||||
struct AInputEvent;
|
||||
|
||||
namespace tp::android {
|
||||
|
||||
[[nodiscard]] std::string unicodeText(android_app& app, const AInputEvent* event);
|
||||
void setSoftKeyboardVisible(android_app& app, bool visible);
|
||||
[[nodiscard]] EdgeInsets systemInsets(android_app& app);
|
||||
bool openUrl(android_app& app, const std::string& url);
|
||||
|
||||
void extractAssets(android_app& app, const char* directory);
|
||||
}
|
||||
|
|
@ -0,0 +1,505 @@
|
|||
#include "AndroidWindow.hpp"
|
||||
|
||||
#include "AndroidKeys.hpp"
|
||||
#include "AndroidPlatform.hpp"
|
||||
|
||||
#include <android/configuration.h>
|
||||
#include <sys/system_properties.h>
|
||||
#include <android/input.h>
|
||||
#include <android/log.h>
|
||||
#include <android/native_activity.h>
|
||||
#include <android/native_window.h>
|
||||
#include <android_native_app_glue.h>
|
||||
#include <vulkan/vulkan_android.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
constexpr const char* kTag = "modules";
|
||||
constexpr halnf kReferenceDensity = 160.0f;
|
||||
constexpr halnf kWheelStep = 1.0f;
|
||||
|
||||
PointerDevice deviceFor(int32_t type) {
|
||||
switch (type) {
|
||||
case AMOTION_EVENT_TOOL_TYPE_STYLUS:
|
||||
case AMOTION_EVENT_TOOL_TYPE_ERASER: return PointerDevice::Stylus;
|
||||
case AMOTION_EVENT_TOOL_TYPE_FINGER: return PointerDevice::Finger;
|
||||
default: return PointerDevice::Mouse;
|
||||
}
|
||||
}
|
||||
|
||||
StylusTip tipFor(int32_t type) {
|
||||
return type == AMOTION_EVENT_TOOL_TYPE_ERASER ? StylusTip::Eraser : StylusTip::Pen;
|
||||
}
|
||||
|
||||
std::optional<PointerButton> buttonFor(int32_t androidButton) {
|
||||
switch (androidButton) {
|
||||
case AMOTION_EVENT_BUTTON_PRIMARY: return PointerButton::Left;
|
||||
case AMOTION_EVENT_BUTTON_SECONDARY: return PointerButton::Right;
|
||||
case AMOTION_EVENT_BUTTON_TERTIARY: return PointerButton::Middle;
|
||||
case AMOTION_EVENT_BUTTON_BACK: return PointerButton::Back;
|
||||
case AMOTION_EVENT_BUTTON_FORWARD: return PointerButton::Forward;
|
||||
default: return std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AndroidWindow::AndroidWindow(PlatformWindowHost& host, android_app& app) :
|
||||
mHost(host),
|
||||
mApp(app) {
|
||||
mFrame.setTranslucent(false);
|
||||
mFrame.setTiled(true);
|
||||
|
||||
mApp.userData = this;
|
||||
mApp.onAppCmd = onCommand;
|
||||
mApp.onInputEvent = onInput;
|
||||
|
||||
if (mApp.config) {
|
||||
const int32_t density = AConfiguration_getDensity(mApp.config);
|
||||
if (density > 0 && density != ACONFIGURATION_DENSITY_ANY) {
|
||||
mScale = (halnf) density / kReferenceDensity;
|
||||
}
|
||||
}
|
||||
|
||||
while (!mApp.window && !mApp.destroyRequested) {
|
||||
pollOnce(-1);
|
||||
}
|
||||
if (mApp.window) {
|
||||
attachWindow(mApp.window);
|
||||
}
|
||||
}
|
||||
|
||||
AndroidWindow::~AndroidWindow() {
|
||||
mApp.onAppCmd = nullptr;
|
||||
mApp.onInputEvent = nullptr;
|
||||
mApp.userData = nullptr;
|
||||
}
|
||||
|
||||
VkSurfaceKHR AndroidWindow::createSurface(VkInstance instance) const {
|
||||
if (!mNativeWindow) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
VkAndroidSurfaceCreateInfoKHR info{
|
||||
.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR,
|
||||
.window = mNativeWindow,
|
||||
};
|
||||
VkSurfaceKHR surface = VK_NULL_HANDLE;
|
||||
if (vkCreateAndroidSurfaceKHR(instance, &info, nullptr, &surface) != VK_SUCCESS) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
return surface;
|
||||
}
|
||||
|
||||
VkExtent2D AndroidWindow::framebufferExtent() const {
|
||||
if (!mNativeWindow) {
|
||||
return { 0, 0 };
|
||||
}
|
||||
return { (uint32_t) ANativeWindow_getWidth(mNativeWindow), (uint32_t) ANativeWindow_getHeight(mNativeWindow) };
|
||||
}
|
||||
|
||||
void AndroidWindow::onTranslucencyResolved(bool) { mFrame.setTranslucent(false); }
|
||||
|
||||
void AndroidWindow::updateGeometry() {
|
||||
if (!mNativeWindow) {
|
||||
return;
|
||||
}
|
||||
const halnf width = (halnf) ANativeWindow_getWidth(mNativeWindow) / mScale;
|
||||
const halnf height = (halnf) ANativeWindow_getHeight(mNativeWindow) / mScale;
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const EdgeInsets pixels = android::systemInsets(mApp);
|
||||
const EdgeInsets insets = {
|
||||
pixels.left / mScale, pixels.top / mScale, pixels.right / mScale, pixels.bottom / mScale
|
||||
};
|
||||
const bool sizeChanged = Vec2F(width, height) != mFrame.contentSize();
|
||||
if (!sizeChanged && insets == mSafeInsets) {
|
||||
return;
|
||||
}
|
||||
|
||||
mSafeInsets = insets;
|
||||
mFrame.setContentSize({ width, height });
|
||||
if (mConfigured) {
|
||||
mHost.onContentResized();
|
||||
}
|
||||
}
|
||||
|
||||
void AndroidWindow::attachWindow(ANativeWindow* window) {
|
||||
mNativeWindow = window;
|
||||
updateGeometry();
|
||||
mHost.onSurfaceAvailable();
|
||||
__android_log_print(
|
||||
ANDROID_LOG_INFO,
|
||||
kTag,
|
||||
"window %dx%d scale %.2f",
|
||||
ANativeWindow_getWidth(window),
|
||||
ANativeWindow_getHeight(window),
|
||||
mScale
|
||||
);
|
||||
}
|
||||
|
||||
void AndroidWindow::detachWindow() {
|
||||
mHost.onSurfaceLost();
|
||||
mNativeWindow = nullptr;
|
||||
}
|
||||
|
||||
void AndroidWindow::onCommand(android_app* app, int32_t command) {
|
||||
if (auto* self = static_cast<AndroidWindow*>(app->userData)) {
|
||||
self->handleCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
int32_t AndroidWindow::onInput(android_app* app, AInputEvent* event) {
|
||||
auto* self = static_cast<AndroidWindow*>(app->userData);
|
||||
return self && self->handleInput(event) ? 1 : 0;
|
||||
}
|
||||
|
||||
void AndroidWindow::handleCommand(int32_t command) {
|
||||
switch (command) {
|
||||
case APP_CMD_INIT_WINDOW:
|
||||
if (mApp.window) {
|
||||
attachWindow(mApp.window);
|
||||
}
|
||||
break;
|
||||
case APP_CMD_TERM_WINDOW: detachWindow(); break;
|
||||
case APP_CMD_WINDOW_RESIZED:
|
||||
case APP_CMD_CONFIG_CHANGED:
|
||||
case APP_CMD_CONTENT_RECT_CHANGED: updateGeometry(); break;
|
||||
case APP_CMD_GAINED_FOCUS:
|
||||
mFocused = true;
|
||||
input().post(KeyboardFocusChanged{ true });
|
||||
break;
|
||||
case APP_CMD_LOST_FOCUS:
|
||||
mFocused = false;
|
||||
input().post(KeyboardFocusChanged{ false });
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
bool AndroidWindow::handleInput(AInputEvent* event) {
|
||||
switch (AInputEvent_getType(event)) {
|
||||
case AINPUT_EVENT_TYPE_MOTION: return handleMotion(event);
|
||||
case AINPUT_EVENT_TYPE_KEY: return handleKey(event);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool AndroidWindow::handleMotion(AInputEvent* event) {
|
||||
const int32_t action = AMotionEvent_getAction(event) & AMOTION_EVENT_ACTION_MASK;
|
||||
const int32_t toolType = AMotionEvent_getToolType(event, 0);
|
||||
if (toolType == AMOTION_EVENT_TOOL_TYPE_STYLUS || toolType == AMOTION_EVENT_TOOL_TYPE_ERASER) {
|
||||
mPenSeen = true;
|
||||
}
|
||||
if (toolType == AMOTION_EVENT_TOOL_TYPE_MOUSE) {
|
||||
mMouseSeen = true;
|
||||
}
|
||||
InputSink& input = this->input();
|
||||
|
||||
if (toolType == AMOTION_EVENT_TOOL_TYPE_FINGER && handleTouchGesture(event, action)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto position = [&](size_t history) -> Vec2F {
|
||||
if (history == SIZE_MAX) {
|
||||
return { AMotionEvent_getX(event, 0) / mScale, AMotionEvent_getY(event, 0) / mScale };
|
||||
}
|
||||
return { AMotionEvent_getHistoricalX(event, 0, history) / mScale,
|
||||
AMotionEvent_getHistoricalY(event, 0, history) / mScale };
|
||||
};
|
||||
const auto sample = [&](size_t history) {
|
||||
PointerSample result;
|
||||
result.device = deviceFor(toolType);
|
||||
result.tip = tipFor(toolType);
|
||||
const bool current = history == SIZE_MAX;
|
||||
result.pressure =
|
||||
current ? AMotionEvent_getPressure(event, 0) : AMotionEvent_getHistoricalPressure(event, 0, history);
|
||||
const halnf tilt = current ? AMotionEvent_getAxisValue(event, AMOTION_EVENT_AXIS_TILT, 0)
|
||||
: AMotionEvent_getHistoricalAxisValue(event, AMOTION_EVENT_AXIS_TILT, 0, history);
|
||||
const halnf orientation =
|
||||
current ? AMotionEvent_getOrientation(event, 0) : AMotionEvent_getHistoricalOrientation(event, 0, history);
|
||||
result.tilt = { std::sin(orientation) * tilt, -std::cos(orientation) * tilt };
|
||||
result.rotation = orientation;
|
||||
result.timeMs =
|
||||
(uint32_t) ((current ? AMotionEvent_getEventTime(event) : AMotionEvent_getHistoricalEventTime(event, history)) /
|
||||
1000000);
|
||||
if (!result.isStylus()) {
|
||||
result.pressure = 1;
|
||||
} else if (result.pressure > 0) {
|
||||
mLastContactPressure = result.pressure;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
switch (action) {
|
||||
case AMOTION_EVENT_ACTION_DOWN:
|
||||
case AMOTION_EVENT_ACTION_BUTTON_PRESS:
|
||||
{
|
||||
input.post(PointerSampled{ sample(SIZE_MAX) });
|
||||
input.post(PointerMoved{ position(SIZE_MAX) });
|
||||
if (action == AMOTION_EVENT_ACTION_BUTTON_PRESS) {
|
||||
deliverButtonChanges(input, AMotionEvent_getButtonState(event));
|
||||
} else if (
|
||||
!mPrimaryHeld && (AMotionEvent_getButtonState(event) == 0 || toolType != AMOTION_EVENT_TOOL_TYPE_MOUSE)
|
||||
) {
|
||||
mPrimaryHeld = true;
|
||||
input.post(PointerButtonChanged{ PointerButton::Left, true });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case AMOTION_EVENT_ACTION_MOVE:
|
||||
case AMOTION_EVENT_ACTION_HOVER_MOVE:
|
||||
{
|
||||
const size_t history = AMotionEvent_getHistorySize(event);
|
||||
for (size_t index = 0; index < history; index++) {
|
||||
input.post(PointerSampled{ sample(index) });
|
||||
input.post(PointerMoved{ position(index) });
|
||||
}
|
||||
input.post(PointerSampled{ sample(SIZE_MAX) });
|
||||
input.post(PointerMoved{ position(SIZE_MAX) });
|
||||
return true;
|
||||
}
|
||||
case AMOTION_EVENT_ACTION_UP:
|
||||
case AMOTION_EVENT_ACTION_CANCEL:
|
||||
case AMOTION_EVENT_ACTION_BUTTON_RELEASE:
|
||||
{
|
||||
PointerSample released = sample(SIZE_MAX);
|
||||
if (released.isStylus()) {
|
||||
released.pressure = mLastContactPressure;
|
||||
}
|
||||
input.post(PointerSampled{ released });
|
||||
input.post(PointerMoved{ position(SIZE_MAX) });
|
||||
if (action == AMOTION_EVENT_ACTION_BUTTON_RELEASE) {
|
||||
deliverButtonChanges(input, AMotionEvent_getButtonState(event));
|
||||
} else if (mPrimaryHeld) {
|
||||
mPrimaryHeld = false;
|
||||
input.post(PointerButtonChanged{ PointerButton::Left, false });
|
||||
}
|
||||
if (toolType == AMOTION_EVENT_TOOL_TYPE_FINGER) {
|
||||
input.post(PointerLeft{});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case AMOTION_EVENT_ACTION_HOVER_EXIT: input.post(PointerLeft{}); return true;
|
||||
case AMOTION_EVENT_ACTION_SCROLL:
|
||||
{
|
||||
const Vec2F delta = { -AMotionEvent_getAxisValue(event, AMOTION_EVENT_AXIS_HSCROLL, 0) * kWheelStep,
|
||||
AMotionEvent_getAxisValue(event, AMOTION_EVENT_AXIS_VSCROLL, 0) * kWheelStep };
|
||||
input.post(PointerMoved{ position(SIZE_MAX) });
|
||||
input.post(Scrolled{ delta, ScrollSource::Wheel });
|
||||
return true;
|
||||
}
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool AndroidWindow::handleTouchGesture(AInputEvent* event, int32_t action) {
|
||||
const size_t count = AMotionEvent_getPointerCount(event);
|
||||
const bool lifting = action == AMOTION_EVENT_ACTION_POINTER_UP;
|
||||
const size_t liftedIndex =
|
||||
lifting ? (size_t) ((AMotionEvent_getAction(event) & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >>
|
||||
AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT)
|
||||
: SIZE_MAX;
|
||||
const bool ending = action == AMOTION_EVENT_ACTION_UP || action == AMOTION_EVENT_ACTION_CANCEL;
|
||||
|
||||
std::vector<TouchPoint> touches;
|
||||
if (!ending) {
|
||||
for (size_t index = 0; index < count; index++) {
|
||||
if (index == liftedIndex) {
|
||||
continue;
|
||||
}
|
||||
touches.push_back(
|
||||
{ AMotionEvent_getPointerId(event, index),
|
||||
{ AMotionEvent_getX(event, index) / mScale, AMotionEvent_getY(event, index) / mScale } }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (touches.size() < 2 && !mGestures.isActive() && !mGestureHoldsPointer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
InputSink& input = this->input();
|
||||
std::vector<TouchGesture> gestures;
|
||||
mGestures.update(touches, gestures, (uint32_t) (AMotionEvent_getEventTime(event) / 1000000));
|
||||
for (const TouchGesture& gesture : gestures) {
|
||||
input.post(GestureChanged{ gesture });
|
||||
}
|
||||
|
||||
if (touches.size() >= 2 && !mGestureHoldsPointer) {
|
||||
mGestureHoldsPointer = true;
|
||||
if (mPrimaryHeld) {
|
||||
mPrimaryHeld = false;
|
||||
input.post(PointerButtonChanged{ PointerButton::Left, false });
|
||||
}
|
||||
input.post(PointerLeft{});
|
||||
}
|
||||
|
||||
if (ending) {
|
||||
mGestureHoldsPointer = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AndroidWindow::deliverButtonChanges(InputSink& input, int32_t state) {
|
||||
const int32_t changed = state ^ mButtonState;
|
||||
for (int32_t bit = 1; bit <= AMOTION_EVENT_BUTTON_FORWARD; bit <<= 1) {
|
||||
if (!(changed & bit)) {
|
||||
continue;
|
||||
}
|
||||
if (auto button = buttonFor(bit)) {
|
||||
input.post(PointerButtonChanged{ *button, (state & bit) != 0 });
|
||||
}
|
||||
}
|
||||
mButtonState = state;
|
||||
}
|
||||
|
||||
bool AndroidWindow::handleKey(AInputEvent* event) {
|
||||
const int32_t keyCode = AKeyEvent_getKeyCode(event);
|
||||
if (keyCode == AKEYCODE_VOLUME_UP || keyCode == AKEYCODE_VOLUME_DOWN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32_t action = AKeyEvent_getAction(event);
|
||||
InputSink& input = this->input();
|
||||
const std::optional<Key> key =
|
||||
keyCode == AKEYCODE_BACK ? std::optional(Key::Escape) : android::keyFromAndroid(keyCode);
|
||||
if (key && action == AKEY_EVENT_ACTION_DOWN) {
|
||||
const bool repeats = AKeyEvent_getRepeatCount(event) > 0;
|
||||
input.post(KeyChanged{ *key, repeats ? KeyAction::Repeat : KeyAction::Press });
|
||||
} else if (key && action == AKEY_EVENT_ACTION_UP) {
|
||||
input.post(KeyChanged{ *key, KeyAction::Release });
|
||||
}
|
||||
|
||||
if (action == AKEY_EVENT_ACTION_DOWN) {
|
||||
const std::string text = android::unicodeText(mApp, event);
|
||||
if (!text.empty()) {
|
||||
input.post(TextEntered{ text });
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AndroidWindow::pollOnce(int timeoutMs) {
|
||||
int events = 0;
|
||||
android_poll_source* source = nullptr;
|
||||
if (ALooper_pollOnce(timeoutMs, nullptr, &events, (void**) &source) >= 0 && source) {
|
||||
source->process(&mApp, source);
|
||||
}
|
||||
}
|
||||
|
||||
void AndroidWindow::wake() {
|
||||
if (mApp.looper) {
|
||||
ALooper_wake(mApp.looper);
|
||||
}
|
||||
}
|
||||
|
||||
void AndroidWindow::dispatchEvents(halnf timeoutMs) {
|
||||
int timeout = timeoutMs < 0 ? -1 : (int) std::ceil(timeoutMs);
|
||||
if (!mNativeWindow && timeout >= 0) {
|
||||
timeout = 100;
|
||||
}
|
||||
pollOnce(timeout);
|
||||
while (true) {
|
||||
int events = 0;
|
||||
android_poll_source* source = nullptr;
|
||||
if (ALooper_pollOnce(0, nullptr, &events, (void**) &source) < 0) {
|
||||
break;
|
||||
}
|
||||
if (source) {
|
||||
source->process(&mApp, source);
|
||||
}
|
||||
if (mApp.destroyRequested) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AndroidWindow::waitUntilConfigured() {
|
||||
while (!mNativeWindow && !mApp.destroyRequested) {
|
||||
pollOnce(-1);
|
||||
}
|
||||
updateGeometry();
|
||||
mConfigured = true;
|
||||
mHost.onContentResized();
|
||||
__android_log_print(ANDROID_LOG_INFO, kTag, "platform %s", describe(platformInfo()).c_str());
|
||||
}
|
||||
|
||||
bool AndroidWindow::shouldClose() const { return mApp.destroyRequested != 0; }
|
||||
|
||||
void AndroidWindow::requestClose() { ANativeActivity_finish(mApp.activity); }
|
||||
|
||||
void AndroidWindow::minimize() { ANativeActivity_finish(mApp.activity); }
|
||||
|
||||
bool AndroidWindow::openUrl(const std::string& url) { return android::openUrl(mApp, url); }
|
||||
|
||||
void AndroidWindow::setTextInputEnabled(bool enabled) {
|
||||
if (enabled == mTextInput) {
|
||||
return;
|
||||
}
|
||||
mTextInput = enabled;
|
||||
android::setSoftKeyboardVisible(mApp, enabled);
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::string systemProperty(const char* name) {
|
||||
char value[PROP_VALUE_MAX] = {};
|
||||
__system_property_get(name, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
constexpr int32_t kTabletSmallestWidthDp = 600;
|
||||
}
|
||||
|
||||
PlatformInfo AndroidWindow::platformInfo() const {
|
||||
PlatformInfo info;
|
||||
info.system = OperatingSystem::Android;
|
||||
info.systemVersion = systemProperty("ro.build.version.release");
|
||||
info.deviceName = systemProperty("ro.product.manufacturer") + " " + systemProperty("ro.product.model");
|
||||
info.contentScale = mScale;
|
||||
info.screenSize = mFrame.contentSize();
|
||||
info.formFactor = FormFactor::Phone;
|
||||
if (AConfiguration* config = mApp.config) {
|
||||
const int32_t smallest = AConfiguration_getSmallestScreenWidthDp(config);
|
||||
if (smallest >= kTabletSmallestWidthDp) {
|
||||
info.formFactor = FormFactor::Tablet;
|
||||
}
|
||||
if (AConfiguration_getUiModeType(config) == ACONFIGURATION_UI_MODE_TYPE_DESK) {
|
||||
info.formFactor = FormFactor::Desktop;
|
||||
}
|
||||
info.hasTouch = AConfiguration_getTouchscreen(config) == ACONFIGURATION_TOUCHSCREEN_FINGER;
|
||||
info.hasPhysicalKeyboard = AConfiguration_getKeyboard(config) == ACONFIGURATION_KEYBOARD_QWERTY;
|
||||
}
|
||||
info.safeInsets = mSafeInsets;
|
||||
info.hasPen = mPenSeen;
|
||||
info.hasMouse = mMouseSeen;
|
||||
info.hasHoverPointer = mMouseSeen || mPenSeen;
|
||||
return info;
|
||||
}
|
||||
|
||||
std::vector<const char*> platformSurfaceExtensions() {
|
||||
return { VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME };
|
||||
}
|
||||
|
||||
// runAndroidApplication extracts the packaged assets into the app's internal storage, which it exports as HOME.
|
||||
std::filesystem::path platformResourceDirectory() {
|
||||
const char* home = std::getenv("HOME");
|
||||
return home ? std::filesystem::path(home) / "rsc" : std::filesystem::path("rsc");
|
||||
}
|
||||
|
||||
std::unique_ptr<PlatformWindow>
|
||||
createPlatformWindow(PlatformWindowHost& host, Vec2F, const WindowIdentity&, const PlatformContext& context) {
|
||||
if (!context.androidApp) {
|
||||
throw std::runtime_error("the Android backend needs PlatformContext::androidApp");
|
||||
}
|
||||
return std::make_unique<AndroidWindow>(host, *context.androidApp);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
#pragma once
|
||||
|
||||
#include "PlatformWindow.hpp"
|
||||
#include "TouchGesture.hpp"
|
||||
|
||||
struct android_app;
|
||||
struct ANativeWindow;
|
||||
struct AInputEvent;
|
||||
|
||||
namespace tp {
|
||||
|
||||
class AndroidWindow final : public PlatformWindow {
|
||||
public:
|
||||
AndroidWindow(PlatformWindowHost& host, android_app& app);
|
||||
~AndroidWindow() override;
|
||||
|
||||
AndroidWindow(const AndroidWindow&) = delete;
|
||||
AndroidWindow& operator=(const AndroidWindow&) = delete;
|
||||
|
||||
[[nodiscard]] VkSurfaceKHR createSurface(VkInstance instance) const override;
|
||||
[[nodiscard]] VkExtent2D framebufferExtent() const override;
|
||||
[[nodiscard]] bool wantsTranslucency() const override { return false; }
|
||||
void onTranslucencyResolved(bool translucent) override;
|
||||
|
||||
void setAcceptsInput(bool) override {}
|
||||
|
||||
void dispatchEvents(halnf timeoutMs) override;
|
||||
void wake() override;
|
||||
void waitUntilConfigured() override;
|
||||
[[nodiscard]] bool hasPendingConfigure() const override { return false; }
|
||||
void applyPendingConfigure() override {}
|
||||
void commitFrameGeometry() override {}
|
||||
[[nodiscard]] bool isWaitingForFrame() const override { return mNativeWindow == nullptr; }
|
||||
|
||||
[[nodiscard]] bool shouldClose() const override;
|
||||
void requestClose() override;
|
||||
|
||||
[[nodiscard]] const WindowFrame& frame() const override { return mFrame; }
|
||||
[[nodiscard]] halnf scale() const override { return mScale; }
|
||||
[[nodiscard]] bool isMaximized() const override { return true; }
|
||||
[[nodiscard]] bool isFocused() const override { return mFocused; }
|
||||
|
||||
void minimize() override;
|
||||
[[nodiscard]] bool isFullScreen() const override { return true; }
|
||||
|
||||
void setCursor(CursorShape) override {}
|
||||
void setClipboardText(const std::string& text) override { mClipboard = text; }
|
||||
[[nodiscard]] std::string clipboardText() override { return mClipboard; }
|
||||
[[nodiscard]] ClipboardImage clipboardImage() override { return {}; }
|
||||
bool openUrl(const std::string& url) override;
|
||||
void setTextInputEnabled(bool enabled) override;
|
||||
void setTextInputRect(const RectF&) override {}
|
||||
|
||||
[[nodiscard]] PlatformInfo platformInfo() const override;
|
||||
|
||||
private:
|
||||
static void onCommand(android_app* app, int32_t command);
|
||||
static int32_t onInput(android_app* app, AInputEvent* event);
|
||||
|
||||
void handleCommand(int32_t command);
|
||||
[[nodiscard]] bool handleInput(AInputEvent* event);
|
||||
[[nodiscard]] bool handleMotion(AInputEvent* event);
|
||||
[[nodiscard]] bool handleKey(AInputEvent* event);
|
||||
void deliverButtonChanges(InputSink& input, int32_t state);
|
||||
[[nodiscard]] bool handleTouchGesture(AInputEvent* event, int32_t action);
|
||||
|
||||
void attachWindow(ANativeWindow* window);
|
||||
void detachWindow();
|
||||
void updateGeometry();
|
||||
void pollOnce(int timeoutMs);
|
||||
[[nodiscard]] InputSink& input() { return mHost.rawInput(); }
|
||||
|
||||
PlatformWindowHost& mHost;
|
||||
android_app& mApp;
|
||||
ANativeWindow* mNativeWindow = nullptr;
|
||||
WindowFrame mFrame;
|
||||
std::string mClipboard;
|
||||
halnf mScale = 1;
|
||||
bool mFocused = false;
|
||||
bool mConfigured = false;
|
||||
bool mPrimaryHeld = false;
|
||||
bool mTextInput = false;
|
||||
EdgeInsets mSafeInsets;
|
||||
bool mPenSeen = false;
|
||||
bool mMouseSeen = false;
|
||||
int32_t mButtonState = 0;
|
||||
halnf mLastContactPressure = 1;
|
||||
TouchGestureTracker mGestures;
|
||||
bool mGestureHoldsPointer = false;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
struct android_app;
|
||||
|
||||
namespace tp {
|
||||
|
||||
using AndroidMain = int (*)(android_app*);
|
||||
|
||||
// Extracts assets, runs main and finishes the activity when main returns or fails.
|
||||
void runAndroidApplication(
|
||||
android_app* app, AndroidMain main, const char* const* assetDirectories, int assetDirectoryCount
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
#include "PlatformInfo.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
namespace tp {
|
||||
|
||||
const char* toString(OperatingSystem system) {
|
||||
switch (system) {
|
||||
case OperatingSystem::Linux: return "Linux";
|
||||
case OperatingSystem::Android: return "Android";
|
||||
case OperatingSystem::Windows: return "Windows";
|
||||
case OperatingSystem::MacOS: return "macOS";
|
||||
case OperatingSystem::IOS: return "iOS";
|
||||
case OperatingSystem::Unknown: break;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* toString(FormFactor formFactor) {
|
||||
switch (formFactor) {
|
||||
case FormFactor::Desktop: return "desktop";
|
||||
case FormFactor::Tablet: return "tablet";
|
||||
case FormFactor::Phone: return "phone";
|
||||
case FormFactor::Unknown: break;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
std::string describe(const PlatformInfo& info) {
|
||||
char text[512];
|
||||
std::snprintf(
|
||||
text,
|
||||
sizeof(text),
|
||||
"%s %s, %s%s%s, scale %.2f, screen %.0fx%.0f, input:%s%s%s%s",
|
||||
toString(info.system),
|
||||
info.systemVersion.c_str(),
|
||||
toString(info.formFactor),
|
||||
info.deviceName.empty() ? "" : " ",
|
||||
info.deviceName.c_str(),
|
||||
info.contentScale,
|
||||
info.screenSize.x,
|
||||
info.screenSize.y,
|
||||
info.hasMouse ? " mouse" : "",
|
||||
info.hasTouch ? " touch" : "",
|
||||
info.hasPen ? " pen" : "",
|
||||
info.hasPhysicalKeyboard ? " keyboard" : ""
|
||||
);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
#include "WindowFrame.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace tp;
|
||||
|
||||
CursorShape tp::cursorForEdge(WindowEdge edge) {
|
||||
switch (edge) {
|
||||
case WindowEdge::Left:
|
||||
case WindowEdge::Right: return CursorShape::ResizeHorizontal;
|
||||
case WindowEdge::Top:
|
||||
case WindowEdge::Bottom: return CursorShape::ResizeVertical;
|
||||
case WindowEdge::TopLeft:
|
||||
case WindowEdge::BottomRight: return CursorShape::ResizeNorthWestSouthEast;
|
||||
case WindowEdge::TopRight:
|
||||
case WindowEdge::BottomLeft: return CursorShape::ResizeNorthEastSouthWest;
|
||||
case WindowEdge::None: return CursorShape::Default;
|
||||
}
|
||||
return CursorShape::Default;
|
||||
}
|
||||
|
||||
Vec2F WindowFrame::surfaceSize() const {
|
||||
const halnf inset = margin();
|
||||
return { mContentSize.x + inset * 2, mContentSize.y + inset * 2 };
|
||||
}
|
||||
|
||||
RectF WindowFrame::contentRect() const {
|
||||
const halnf inset = margin();
|
||||
return { inset, inset, mContentSize.x, mContentSize.y };
|
||||
}
|
||||
|
||||
RectF WindowFrame::inputRect() const {
|
||||
const RectF content = contentRect();
|
||||
const halnf outer = outerResizeBorder();
|
||||
return { content.x - outer, content.y - outer, content.z + outer * 2, content.w + outer * 2 };
|
||||
}
|
||||
|
||||
halnf WindowFrame::outerResizeBorder() const { return std::min(kResizeBorder, margin()); }
|
||||
|
||||
halnf WindowFrame::innerResizeBorder() const {
|
||||
return std::max(kMinimumInnerResizeBorder, kResizeBorder - outerResizeBorder());
|
||||
}
|
||||
|
||||
WindowEdge WindowFrame::edgeAt(Vec2F point) const {
|
||||
if (mTiled) {
|
||||
return WindowEdge::None;
|
||||
}
|
||||
|
||||
const RectF zone = inputRect();
|
||||
const bool insideZone =
|
||||
point.x >= zone.x && point.y >= zone.y && point.x < zone.x + zone.z && point.y < zone.y + zone.w;
|
||||
if (!insideZone) {
|
||||
return WindowEdge::None;
|
||||
}
|
||||
|
||||
const RectF content = contentRect();
|
||||
const halnf inner = innerResizeBorder();
|
||||
|
||||
const bool onLeft = point.x < content.x + inner;
|
||||
const bool onRight = point.x >= content.x + content.z - inner;
|
||||
const bool onTop = point.y < content.y + inner;
|
||||
const bool onBottom = point.y >= content.y + content.w - inner;
|
||||
|
||||
const bool nearLeft = point.x < content.x + kCornerResizeLength;
|
||||
const bool nearRight = point.x >= content.x + content.z - kCornerResizeLength;
|
||||
const bool nearTop = point.y < content.y + kCornerResizeLength;
|
||||
const bool nearBottom = point.y >= content.y + content.w - kCornerResizeLength;
|
||||
|
||||
if ((onTop && nearLeft) || (onLeft && nearTop)) {
|
||||
return WindowEdge::TopLeft;
|
||||
}
|
||||
if ((onTop && nearRight) || (onRight && nearTop)) {
|
||||
return WindowEdge::TopRight;
|
||||
}
|
||||
if ((onBottom && nearLeft) || (onLeft && nearBottom)) {
|
||||
return WindowEdge::BottomLeft;
|
||||
}
|
||||
if ((onBottom && nearRight) || (onRight && nearBottom)) {
|
||||
return WindowEdge::BottomRight;
|
||||
}
|
||||
if (onTop) {
|
||||
return WindowEdge::Top;
|
||||
}
|
||||
if (onBottom) {
|
||||
return WindowEdge::Bottom;
|
||||
}
|
||||
if (onLeft) {
|
||||
return WindowEdge::Left;
|
||||
}
|
||||
if (onRight) {
|
||||
return WindowEdge::Right;
|
||||
}
|
||||
return WindowEdge::None;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
#include "Vec.hpp"
|
||||
#include "WindowTypes.hpp"
|
||||
|
||||
namespace tp {
|
||||
|
||||
// Window manipulation that only desktop window systems offer; absent on other platforms.
|
||||
class DesktopWindowControls {
|
||||
public:
|
||||
virtual ~DesktopWindowControls() = default;
|
||||
|
||||
virtual void toggleMaximize() = 0;
|
||||
virtual void setFullScreen(bool fullScreen) = 0;
|
||||
virtual void beginMove() = 0;
|
||||
virtual void beginResize(WindowEdge edge) = 0;
|
||||
virtual void showWindowMenu(Vec2F contentPosition) = 0;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
struct android_app;
|
||||
|
||||
namespace tp {
|
||||
|
||||
// Process objects a backend needs to open a window; desktop backends need none.
|
||||
struct PlatformContext {
|
||||
android_app* androidApp = nullptr;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
#pragma once
|
||||
|
||||
#include "Vec.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace tp {
|
||||
|
||||
enum class OperatingSystem { Linux, Android, Windows, MacOS, IOS, Unknown };
|
||||
|
||||
enum class FormFactor { Desktop, Tablet, Phone, Unknown };
|
||||
|
||||
struct EdgeInsets {
|
||||
halnf left = 0;
|
||||
halnf top = 0;
|
||||
halnf right = 0;
|
||||
halnf bottom = 0;
|
||||
|
||||
[[nodiscard]] bool operator==(const EdgeInsets& other) const = default;
|
||||
};
|
||||
|
||||
struct PlatformInfo {
|
||||
OperatingSystem system = OperatingSystem::Unknown;
|
||||
FormFactor formFactor = FormFactor::Unknown;
|
||||
std::string systemVersion;
|
||||
std::string deviceName;
|
||||
|
||||
halnf contentScale = 1;
|
||||
Vec2F screenSize = { 0, 0 };
|
||||
EdgeInsets safeInsets;
|
||||
|
||||
bool hasMouse = false;
|
||||
bool hasTouch = false;
|
||||
bool hasPen = false;
|
||||
bool hasPhysicalKeyboard = false;
|
||||
|
||||
bool hasHoverPointer = false;
|
||||
|
||||
[[nodiscard]] bool isTouchFirst() const {
|
||||
return formFactor == FormFactor::Tablet || formFactor == FormFactor::Phone;
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] const char* toString(OperatingSystem system);
|
||||
[[nodiscard]] const char* toString(FormFactor formFactor);
|
||||
[[nodiscard]] std::string describe(const PlatformInfo& info);
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputSink.hpp"
|
||||
#include "PlatformContext.hpp"
|
||||
#include "PlatformInfo.hpp"
|
||||
#include "Rect.hpp"
|
||||
#include "WindowFrame.hpp"
|
||||
#include "WindowSurface.hpp"
|
||||
#include "WindowTypes.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
class DesktopWindowControls;
|
||||
|
||||
// What a backend reports to the window that owns it.
|
||||
class PlatformWindowHost {
|
||||
public:
|
||||
virtual ~PlatformWindowHost() = default;
|
||||
|
||||
[[nodiscard]] virtual InputSink& rawInput() = 0;
|
||||
virtual void onContentResized() = 0;
|
||||
virtual void onSurfaceAvailable() = 0;
|
||||
virtual void onSurfaceLost() = 0;
|
||||
};
|
||||
|
||||
class PlatformWindow : public VulkanSurfaceSource {
|
||||
public:
|
||||
~PlatformWindow() override = default;
|
||||
|
||||
virtual void dispatchEvents(halnf timeoutMs) = 0;
|
||||
virtual void wake() = 0;
|
||||
virtual void waitUntilConfigured() = 0;
|
||||
[[nodiscard]] virtual bool hasPendingConfigure() const = 0;
|
||||
virtual void applyPendingConfigure() = 0;
|
||||
virtual void commitFrameGeometry() = 0;
|
||||
[[nodiscard]] virtual bool isWaitingForFrame() const = 0;
|
||||
virtual void setAcceptsInput(bool accepts) = 0;
|
||||
|
||||
[[nodiscard]] virtual bool shouldClose() const = 0;
|
||||
virtual void requestClose() = 0;
|
||||
|
||||
[[nodiscard]] virtual const WindowFrame& frame() const = 0;
|
||||
[[nodiscard]] virtual halnf scale() const = 0;
|
||||
[[nodiscard]] virtual bool isMaximized() const = 0;
|
||||
[[nodiscard]] virtual bool isFullScreen() const = 0;
|
||||
[[nodiscard]] virtual bool isFocused() const = 0;
|
||||
virtual void minimize() = 0;
|
||||
[[nodiscard]] virtual DesktopWindowControls* desktopControls() { return nullptr; }
|
||||
|
||||
virtual void setCursor(CursorShape shape) = 0;
|
||||
virtual void setClipboardText(const std::string& text) = 0;
|
||||
[[nodiscard]] virtual std::string clipboardText() = 0;
|
||||
[[nodiscard]] virtual ClipboardImage clipboardImage() = 0;
|
||||
virtual bool openUrl(const std::string& url) = 0;
|
||||
virtual void setTextInputEnabled(bool enabled) = 0;
|
||||
virtual void setTextInputRect(const RectF& contentRect) = 0;
|
||||
|
||||
[[nodiscard]] virtual PlatformInfo platformInfo() const = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::vector<const char*> platformSurfaceExtensions();
|
||||
|
||||
[[nodiscard]] std::filesystem::path platformResourceDirectory();
|
||||
|
||||
[[nodiscard]] std::unique_ptr<PlatformWindow> createPlatformWindow(
|
||||
PlatformWindowHost& host, Vec2F contentSize, const WindowIdentity& identity, const PlatformContext& context
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
#include "Rect.hpp"
|
||||
#include "WindowTypes.hpp"
|
||||
|
||||
namespace tp {
|
||||
|
||||
class WindowFrame {
|
||||
public:
|
||||
static constexpr halnf kShadowMargin = 18;
|
||||
static constexpr halnf kCornerRadius = 12;
|
||||
static constexpr halnf kResizeBorder = 10;
|
||||
static constexpr halnf kMinimumInnerResizeBorder = 3;
|
||||
static constexpr halnf kCornerResizeLength = 18;
|
||||
|
||||
void setContentSize(Vec2F size) { mContentSize = size; }
|
||||
void setTiled(bool tiled) { mTiled = tiled; }
|
||||
void setTranslucent(bool translucent) { mTranslucent = translucent; }
|
||||
|
||||
[[nodiscard]] Vec2F contentSize() const { return mContentSize; }
|
||||
[[nodiscard]] bool isTiled() const { return mTiled; }
|
||||
[[nodiscard]] bool isTranslucent() const { return mTranslucent; }
|
||||
|
||||
[[nodiscard]] bool hasShadow() const { return mTranslucent && !mTiled; }
|
||||
[[nodiscard]] halnf margin() const { return hasShadow() ? kShadowMargin : 0; }
|
||||
[[nodiscard]] halnf cornerRadius() const { return hasShadow() ? kCornerRadius : 0; }
|
||||
|
||||
[[nodiscard]] Vec2F surfaceSize() const;
|
||||
[[nodiscard]] RectF contentRect() const;
|
||||
[[nodiscard]] RectF inputRect() const;
|
||||
|
||||
[[nodiscard]] WindowEdge edgeAt(Vec2F surfacePoint) const;
|
||||
|
||||
private:
|
||||
[[nodiscard]] halnf outerResizeBorder() const;
|
||||
[[nodiscard]] halnf innerResizeBorder() const;
|
||||
|
||||
Vec2F mContentSize = { 0, 0 };
|
||||
bool mTiled = false;
|
||||
bool mTranslucent = true;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace tp {
|
||||
|
||||
struct ClipboardImage {
|
||||
std::string mimeType;
|
||||
std::string bytes;
|
||||
|
||||
[[nodiscard]] bool isEmpty() const { return bytes.empty(); }
|
||||
};
|
||||
|
||||
enum class CursorShape {
|
||||
Default,
|
||||
Pointer,
|
||||
Text,
|
||||
Move,
|
||||
Grab,
|
||||
Grabbing,
|
||||
ResizeHorizontal,
|
||||
ResizeVertical,
|
||||
ResizeNorthWestSouthEast,
|
||||
ResizeNorthEastSouthWest,
|
||||
NotAllowed,
|
||||
Crosshair,
|
||||
Hidden
|
||||
};
|
||||
|
||||
struct WindowIdentity {
|
||||
std::string appId;
|
||||
std::string title;
|
||||
|
||||
[[nodiscard]] static WindowIdentity generic();
|
||||
};
|
||||
|
||||
enum class WindowEdge { None, Top, Bottom, Left, Right, TopLeft, TopRight, BottomLeft, BottomRight };
|
||||
|
||||
CursorShape cursorForEdge(WindowEdge edge);
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
#include "LinuxInputCodes.hpp"
|
||||
|
||||
#include <linux/input-event-codes.h>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace tp;
|
||||
|
||||
namespace {
|
||||
const std::unordered_map<uint32_t, Key>& evdevKeys() {
|
||||
static const std::unordered_map<uint32_t, Key> keys = {
|
||||
{ KEY_SPACE, Key::Space },
|
||||
{ KEY_APOSTROPHE, Key::Apostrophe },
|
||||
{ KEY_COMMA, Key::Comma },
|
||||
{ KEY_MINUS, Key::Minus },
|
||||
{ KEY_DOT, Key::Period },
|
||||
{ KEY_SLASH, Key::Slash },
|
||||
{ KEY_0, Key::Digit0 },
|
||||
{ KEY_1, Key::Digit1 },
|
||||
{ KEY_2, Key::Digit2 },
|
||||
{ KEY_3, Key::Digit3 },
|
||||
{ KEY_4, Key::Digit4 },
|
||||
{ KEY_5, Key::Digit5 },
|
||||
{ KEY_6, Key::Digit6 },
|
||||
{ KEY_7, Key::Digit7 },
|
||||
{ KEY_8, Key::Digit8 },
|
||||
{ KEY_9, Key::Digit9 },
|
||||
{ KEY_SEMICOLON, Key::Semicolon },
|
||||
{ KEY_EQUAL, Key::Equal },
|
||||
{ KEY_A, Key::A },
|
||||
{ KEY_B, Key::B },
|
||||
{ KEY_C, Key::C },
|
||||
{ KEY_D, Key::D },
|
||||
{ KEY_E, Key::E },
|
||||
{ KEY_F, Key::F },
|
||||
{ KEY_G, Key::G },
|
||||
{ KEY_H, Key::H },
|
||||
{ KEY_I, Key::I },
|
||||
{ KEY_J, Key::J },
|
||||
{ KEY_K, Key::K },
|
||||
{ KEY_L, Key::L },
|
||||
{ KEY_M, Key::M },
|
||||
{ KEY_N, Key::N },
|
||||
{ KEY_O, Key::O },
|
||||
{ KEY_P, Key::P },
|
||||
{ KEY_Q, Key::Q },
|
||||
{ KEY_R, Key::R },
|
||||
{ KEY_S, Key::S },
|
||||
{ KEY_T, Key::T },
|
||||
{ KEY_U, Key::U },
|
||||
{ KEY_V, Key::V },
|
||||
{ KEY_W, Key::W },
|
||||
{ KEY_X, Key::X },
|
||||
{ KEY_Y, Key::Y },
|
||||
{ KEY_Z, Key::Z },
|
||||
{ KEY_LEFTBRACE, Key::LeftBracket },
|
||||
{ KEY_BACKSLASH, Key::Backslash },
|
||||
{ KEY_RIGHTBRACE, Key::RightBracket },
|
||||
{ KEY_GRAVE, Key::GraveAccent },
|
||||
{ KEY_102ND, Key::World2 },
|
||||
{ KEY_ESC, Key::Escape },
|
||||
{ KEY_ENTER, Key::Enter },
|
||||
{ KEY_TAB, Key::Tab },
|
||||
{ KEY_BACKSPACE, Key::Backspace },
|
||||
{ KEY_INSERT, Key::Insert },
|
||||
{ KEY_DELETE, Key::Delete },
|
||||
{ KEY_RIGHT, Key::Right },
|
||||
{ KEY_LEFT, Key::Left },
|
||||
{ KEY_DOWN, Key::Down },
|
||||
{ KEY_UP, Key::Up },
|
||||
{ KEY_PAGEUP, Key::PageUp },
|
||||
{ KEY_PAGEDOWN, Key::PageDown },
|
||||
{ KEY_HOME, Key::Home },
|
||||
{ KEY_END, Key::End },
|
||||
{ KEY_CAPSLOCK, Key::CapsLock },
|
||||
{ KEY_SCROLLLOCK, Key::ScrollLock },
|
||||
{ KEY_NUMLOCK, Key::NumLock },
|
||||
{ KEY_SYSRQ, Key::PrintScreen },
|
||||
{ KEY_PAUSE, Key::Pause },
|
||||
{ KEY_F1, Key::F1 },
|
||||
{ KEY_F2, Key::F2 },
|
||||
{ KEY_F3, Key::F3 },
|
||||
{ KEY_F4, Key::F4 },
|
||||
{ KEY_F5, Key::F5 },
|
||||
{ KEY_F6, Key::F6 },
|
||||
{ KEY_F7, Key::F7 },
|
||||
{ KEY_F8, Key::F8 },
|
||||
{ KEY_F9, Key::F9 },
|
||||
{ KEY_F10, Key::F10 },
|
||||
{ KEY_F11, Key::F11 },
|
||||
{ KEY_F12, Key::F12 },
|
||||
{ KEY_F13, Key::F13 },
|
||||
{ KEY_F14, Key::F14 },
|
||||
{ KEY_F15, Key::F15 },
|
||||
{ KEY_F16, Key::F16 },
|
||||
{ KEY_F17, Key::F17 },
|
||||
{ KEY_F18, Key::F18 },
|
||||
{ KEY_F19, Key::F19 },
|
||||
{ KEY_F20, Key::F20 },
|
||||
{ KEY_F21, Key::F21 },
|
||||
{ KEY_F22, Key::F22 },
|
||||
{ KEY_F23, Key::F23 },
|
||||
{ KEY_F24, Key::F24 },
|
||||
{ KEY_KP0, Key::Keypad0 },
|
||||
{ KEY_KP1, Key::Keypad1 },
|
||||
{ KEY_KP2, Key::Keypad2 },
|
||||
{ KEY_KP3, Key::Keypad3 },
|
||||
{ KEY_KP4, Key::Keypad4 },
|
||||
{ KEY_KP5, Key::Keypad5 },
|
||||
{ KEY_KP6, Key::Keypad6 },
|
||||
{ KEY_KP7, Key::Keypad7 },
|
||||
{ KEY_KP8, Key::Keypad8 },
|
||||
{ KEY_KP9, Key::Keypad9 },
|
||||
{ KEY_KPDOT, Key::KeypadDecimal },
|
||||
{ KEY_KPSLASH, Key::KeypadDivide },
|
||||
{ KEY_KPASTERISK, Key::KeypadMultiply },
|
||||
{ KEY_KPMINUS, Key::KeypadSubtract },
|
||||
{ KEY_KPPLUS, Key::KeypadAdd },
|
||||
{ KEY_KPENTER, Key::KeypadEnter },
|
||||
{ KEY_KPEQUAL, Key::KeypadEqual },
|
||||
{ KEY_LEFTSHIFT, Key::LeftShift },
|
||||
{ KEY_LEFTCTRL, Key::LeftControl },
|
||||
{ KEY_LEFTALT, Key::LeftAlt },
|
||||
{ KEY_LEFTMETA, Key::LeftSuper },
|
||||
{ KEY_RIGHTSHIFT, Key::RightShift },
|
||||
{ KEY_RIGHTCTRL, Key::RightControl },
|
||||
{ KEY_RIGHTALT, Key::RightAlt },
|
||||
{ KEY_RIGHTMETA, Key::RightSuper },
|
||||
{ KEY_COMPOSE, Key::Menu },
|
||||
};
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Key> tp::keyFromEvdev(uint32_t evdevKey) {
|
||||
const auto& keys = evdevKeys();
|
||||
const auto found = keys.find(evdevKey);
|
||||
if (found == keys.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return found->second;
|
||||
}
|
||||
|
||||
std::optional<PointerButton> tp::pointerButtonFromLinux(uint32_t linuxButton) {
|
||||
switch (linuxButton) {
|
||||
case BTN_LEFT: return PointerButton::Left;
|
||||
case BTN_RIGHT: return PointerButton::Right;
|
||||
case BTN_MIDDLE: return PointerButton::Middle;
|
||||
case BTN_SIDE: return PointerButton::Back;
|
||||
case BTN_EXTRA: return PointerButton::Forward;
|
||||
default: return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputCodes.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace tp {
|
||||
|
||||
[[nodiscard]] std::optional<Key> keyFromEvdev(uint32_t evdevKey);
|
||||
|
||||
[[nodiscard]] std::optional<PointerButton> pointerButtonFromLinux(uint32_t linuxButton);
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
#include "PointerPinchConverter.hpp"
|
||||
|
||||
#include <numbers>
|
||||
|
||||
namespace tp {
|
||||
|
||||
TouchGesture PointerPinchConverter::begin(Vec2F center) {
|
||||
mActive = true;
|
||||
mTotalScale = 1;
|
||||
return { GesturePhase::Begin, center };
|
||||
}
|
||||
|
||||
std::optional<TouchGesture>
|
||||
PointerPinchConverter::update(Vec2F center, Vec2F pan, halnf totalScale, halnf rotationDegrees) {
|
||||
if (!mActive) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
TouchGesture gesture;
|
||||
gesture.phase = GesturePhase::Update;
|
||||
gesture.center = center;
|
||||
gesture.pan = pan;
|
||||
gesture.scale = mTotalScale > 0 && totalScale > 0 ? totalScale / mTotalScale : 1;
|
||||
gesture.rotation = wrapAngle(rotationDegrees * std::numbers::pi_v<halnf> / 180);
|
||||
if (totalScale > 0) {
|
||||
mTotalScale = totalScale;
|
||||
}
|
||||
return gesture;
|
||||
}
|
||||
|
||||
std::optional<TouchGesture> PointerPinchConverter::end(Vec2F center) {
|
||||
if (!mActive) {
|
||||
return std::nullopt;
|
||||
}
|
||||
mActive = false;
|
||||
return TouchGesture{ GesturePhase::End, center };
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
#pragma once
|
||||
|
||||
#include "TouchGesture.hpp"
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace tp {
|
||||
|
||||
// Converts touchpad pinch updates (scale relative to the gesture start, pan and rotation per event) into
|
||||
// incremental TouchGesture updates.
|
||||
class PointerPinchConverter {
|
||||
public:
|
||||
[[nodiscard]] TouchGesture begin(Vec2F center);
|
||||
[[nodiscard]] std::optional<TouchGesture> update(Vec2F center, Vec2F pan, halnf totalScale, halnf rotationDegrees);
|
||||
[[nodiscard]] std::optional<TouchGesture> end(Vec2F center);
|
||||
|
||||
private:
|
||||
halnf mTotalScale = 1;
|
||||
bool mActive = false;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
#include "WaylandClipboard.hpp"
|
||||
|
||||
#include "WaylandConnection.hpp"
|
||||
|
||||
#include <wayland-client.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
constexpr const char* kTextMimeType = "text/plain;charset=utf-8";
|
||||
constexpr const char* kLegacyTextMimeType = "text/plain";
|
||||
constexpr const char* kPngMimeType = "image/png";
|
||||
constexpr const char* kImageMimePrefix = "image/";
|
||||
|
||||
WaylandClipboard& clipboard(void* data) { return *static_cast<WaylandClipboard*>(data); }
|
||||
|
||||
void offerMimeType(void* data, wl_data_offer* offer, const char* mimeType) {
|
||||
clipboard(data).onOfferMimeType(offer, mimeType);
|
||||
}
|
||||
void offerSourceActions(void*, wl_data_offer*, uint32_t) {}
|
||||
void offerAction(void*, wl_data_offer*, uint32_t) {}
|
||||
|
||||
void dataOffer(void* data, wl_data_device*, wl_data_offer* offer) { clipboard(data).onOffer(offer); }
|
||||
void dataEnter(void*, wl_data_device*, uint32_t, wl_surface*, wl_fixed_t, wl_fixed_t, wl_data_offer* offer) {
|
||||
if (offer) {
|
||||
wl_data_offer_destroy(offer);
|
||||
}
|
||||
}
|
||||
void dataLeave(void*, wl_data_device*) {}
|
||||
void dataMotion(void*, wl_data_device*, uint32_t, wl_fixed_t, wl_fixed_t) {}
|
||||
void dataDrop(void*, wl_data_device*) {}
|
||||
void dataSelection(void* data, wl_data_device*, wl_data_offer* offer) { clipboard(data).onSelection(offer); }
|
||||
|
||||
void sourceTarget(void*, wl_data_source*, const char*) {}
|
||||
void sourceSend(void* data, wl_data_source*, const char* mimeType, int32_t descriptor) {
|
||||
clipboard(data).onSourceSend(mimeType, descriptor);
|
||||
}
|
||||
void sourceCancelled(void* data, wl_data_source* source) { clipboard(data).onSourceCancelled(source); }
|
||||
void sourceDropPerformed(void*, wl_data_source*) {}
|
||||
void sourceDndFinished(void*, wl_data_source*) {}
|
||||
void sourceAction(void*, wl_data_source*, uint32_t) {}
|
||||
|
||||
const wl_data_offer_listener kOfferListener = { offerMimeType, offerSourceActions, offerAction };
|
||||
|
||||
const wl_data_device_listener kDataDeviceListener = {
|
||||
dataOffer, dataEnter, dataLeave, dataMotion, dataDrop, dataSelection,
|
||||
};
|
||||
|
||||
const wl_data_source_listener kSourceListener = {
|
||||
sourceTarget, sourceSend, sourceCancelled, sourceDropPerformed, sourceDndFinished, sourceAction,
|
||||
};
|
||||
|
||||
void writeAll(int descriptor, const std::string& text) {
|
||||
size_t written = 0;
|
||||
while (written < text.size()) {
|
||||
const ssize_t result = write(descriptor, text.data() + written, text.size() - written);
|
||||
if (result < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (result <= 0) {
|
||||
return;
|
||||
}
|
||||
written += (size_t) result;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string* preferredImageType(const std::vector<std::string>& types) {
|
||||
const std::string* chosen = nullptr;
|
||||
for (const std::string& type : types) {
|
||||
if (type == kPngMimeType) {
|
||||
return &type;
|
||||
}
|
||||
if (!chosen && type.starts_with(kImageMimePrefix)) {
|
||||
chosen = &type;
|
||||
}
|
||||
}
|
||||
return chosen;
|
||||
}
|
||||
|
||||
bool offers(const std::vector<std::string>& types, const char* mimeType) {
|
||||
return std::find(types.begin(), types.end(), mimeType) != types.end();
|
||||
}
|
||||
}
|
||||
|
||||
WaylandClipboard::WaylandClipboard(WaylandSeatContext& seat, wl_data_device_manager* manager, wl_seat* waylandSeat) :
|
||||
mSeat(seat),
|
||||
mManager(manager) {
|
||||
mDevice = wl_data_device_manager_get_data_device(manager, waylandSeat);
|
||||
wl_data_device_add_listener(mDevice, &kDataDeviceListener, this);
|
||||
}
|
||||
|
||||
WaylandClipboard::~WaylandClipboard() {
|
||||
closeTransfer(mText);
|
||||
closeTransfer(mImage);
|
||||
if (mSource) {
|
||||
wl_data_source_destroy(mSource);
|
||||
}
|
||||
if (mSelection) {
|
||||
wl_data_offer_destroy(mSelection);
|
||||
}
|
||||
if (mPendingOffer && mPendingOffer != mSelection) {
|
||||
wl_data_offer_destroy(mPendingOffer);
|
||||
}
|
||||
if (mDevice) {
|
||||
wl_data_device_release(mDevice);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandClipboard::setText(const std::string& text) {
|
||||
mOwnedText = text;
|
||||
if (mSource) {
|
||||
wl_data_source_destroy(mSource);
|
||||
}
|
||||
|
||||
mSource = wl_data_device_manager_create_data_source(mManager);
|
||||
wl_data_source_add_listener(mSource, &kSourceListener, this);
|
||||
wl_data_source_offer(mSource, kTextMimeType);
|
||||
wl_data_source_offer(mSource, kLegacyTextMimeType);
|
||||
wl_data_device_set_selection(mDevice, mSource, mSeat.lastInputSerial());
|
||||
mSeat.connection().flush();
|
||||
}
|
||||
|
||||
std::string WaylandClipboard::text() const {
|
||||
if (mSource) {
|
||||
return mOwnedText;
|
||||
}
|
||||
return mText.complete ? mText.bytes : std::string();
|
||||
}
|
||||
|
||||
ClipboardImage WaylandClipboard::image() const {
|
||||
if (mSource || !mImage.complete) {
|
||||
return {};
|
||||
}
|
||||
return { mImage.mimeType, mImage.bytes };
|
||||
}
|
||||
|
||||
void WaylandClipboard::startTransfer(Transfer& transfer, const std::string& mimeType) {
|
||||
closeTransfer(transfer);
|
||||
transfer = Transfer{ mimeType };
|
||||
|
||||
int pipeDescriptors[2];
|
||||
if (pipe2(pipeDescriptors, O_CLOEXEC | O_NONBLOCK) != 0) {
|
||||
transfer.complete = true;
|
||||
return;
|
||||
}
|
||||
wl_data_offer_receive(mSelection, mimeType.c_str(), pipeDescriptors[1]);
|
||||
close(pipeDescriptors[1]);
|
||||
transfer.descriptor = pipeDescriptors[0];
|
||||
}
|
||||
|
||||
void WaylandClipboard::readAvailable(Transfer& transfer) {
|
||||
char buffer[4096];
|
||||
while (true) {
|
||||
const ssize_t result = read(transfer.descriptor, buffer, sizeof(buffer));
|
||||
if (result > 0) {
|
||||
transfer.bytes.append(buffer, (size_t) result);
|
||||
continue;
|
||||
}
|
||||
if (result < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (result < 0 && errno == EAGAIN) {
|
||||
return;
|
||||
}
|
||||
closeTransfer(transfer);
|
||||
transfer.complete = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandClipboard::closeTransfer(Transfer& transfer) {
|
||||
if (transfer.descriptor >= 0) {
|
||||
close(transfer.descriptor);
|
||||
}
|
||||
transfer.descriptor = -1;
|
||||
}
|
||||
|
||||
void WaylandClipboard::collectPollDescriptors(std::vector<pollfd>& descriptors) const {
|
||||
for (const Transfer* transfer : { &mText, &mImage }) {
|
||||
if (transfer->descriptor >= 0) {
|
||||
descriptors.push_back({ transfer->descriptor, POLLIN, 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandClipboard::onDescriptorsReady(const std::vector<pollfd>& descriptors) {
|
||||
for (const pollfd& ready : descriptors) {
|
||||
if (!(ready.revents & (POLLIN | POLLHUP | POLLERR))) {
|
||||
continue;
|
||||
}
|
||||
for (Transfer* transfer : { &mText, &mImage }) {
|
||||
if (transfer->descriptor == ready.fd) {
|
||||
readAvailable(*transfer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandClipboard::forgetSelection() {
|
||||
closeTransfer(mText);
|
||||
closeTransfer(mImage);
|
||||
mText = {};
|
||||
mImage = {};
|
||||
if (mSelection) {
|
||||
wl_data_offer_destroy(mSelection);
|
||||
}
|
||||
mSelection = nullptr;
|
||||
}
|
||||
|
||||
void WaylandClipboard::onOffer(wl_data_offer* offer) {
|
||||
mPendingOffer = offer;
|
||||
mPendingOfferTypes.clear();
|
||||
wl_data_offer_add_listener(offer, &kOfferListener, this);
|
||||
}
|
||||
|
||||
void WaylandClipboard::onOfferMimeType(wl_data_offer* offer, const char* mimeType) {
|
||||
if (offer == mPendingOffer) {
|
||||
mPendingOfferTypes.emplace_back(mimeType);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandClipboard::onSelection(wl_data_offer* offer) {
|
||||
const std::vector<std::string> types =
|
||||
offer && offer == mPendingOffer ? std::move(mPendingOfferTypes) : std::vector<std::string>{};
|
||||
if (mSelection != offer) {
|
||||
forgetSelection();
|
||||
}
|
||||
mSelection = offer;
|
||||
mPendingOfferTypes.clear();
|
||||
mPendingOffer = nullptr;
|
||||
if (!mSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (offers(types, kTextMimeType)) {
|
||||
startTransfer(mText, kTextMimeType);
|
||||
}
|
||||
if (const std::string* imageType = preferredImageType(types)) {
|
||||
startTransfer(mImage, *imageType);
|
||||
}
|
||||
mSeat.connection().flush();
|
||||
}
|
||||
|
||||
void WaylandClipboard::onSourceSend(const char*, int descriptor) {
|
||||
writeAll(descriptor, mOwnedText);
|
||||
close(descriptor);
|
||||
}
|
||||
|
||||
void WaylandClipboard::onSourceCancelled(wl_data_source* source) {
|
||||
if (source == mSource) {
|
||||
mSource = nullptr;
|
||||
}
|
||||
wl_data_source_destroy(source);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
#pragma once
|
||||
|
||||
#include "WaylandSeatContext.hpp"
|
||||
#include "WindowTypes.hpp"
|
||||
|
||||
#include <poll.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct wl_data_device;
|
||||
struct wl_data_device_manager;
|
||||
struct wl_data_offer;
|
||||
struct wl_data_source;
|
||||
struct wl_seat;
|
||||
|
||||
namespace tp {
|
||||
|
||||
// The selection clipboard. Offered contents are read asynchronously on the event loop, so reading the clipboard
|
||||
// never blocks the UI thread; a selection that is still arriving reads as empty.
|
||||
class WaylandClipboard {
|
||||
public:
|
||||
WaylandClipboard(WaylandSeatContext& seat, wl_data_device_manager* manager, wl_seat* waylandSeat);
|
||||
~WaylandClipboard();
|
||||
|
||||
WaylandClipboard(const WaylandClipboard&) = delete;
|
||||
WaylandClipboard& operator=(const WaylandClipboard&) = delete;
|
||||
|
||||
void setText(const std::string& text);
|
||||
[[nodiscard]] std::string text() const;
|
||||
[[nodiscard]] ClipboardImage image() const;
|
||||
|
||||
void collectPollDescriptors(std::vector<pollfd>& descriptors) const;
|
||||
void onDescriptorsReady(const std::vector<pollfd>& descriptors);
|
||||
|
||||
void onOffer(wl_data_offer* offer);
|
||||
void onOfferMimeType(wl_data_offer* offer, const char* mimeType);
|
||||
void onSelection(wl_data_offer* offer);
|
||||
void onSourceSend(const char* mimeType, int descriptor);
|
||||
void onSourceCancelled(wl_data_source* source);
|
||||
|
||||
private:
|
||||
struct Transfer {
|
||||
std::string mimeType;
|
||||
int descriptor = -1;
|
||||
std::string bytes;
|
||||
bool complete = false;
|
||||
};
|
||||
|
||||
void startTransfer(Transfer& transfer, const std::string& mimeType);
|
||||
static void readAvailable(Transfer& transfer);
|
||||
static void closeTransfer(Transfer& transfer);
|
||||
void forgetSelection();
|
||||
|
||||
WaylandSeatContext& mSeat;
|
||||
wl_data_device_manager* mManager;
|
||||
wl_data_device* mDevice = nullptr;
|
||||
wl_data_offer* mPendingOffer = nullptr;
|
||||
std::vector<std::string> mPendingOfferTypes;
|
||||
wl_data_offer* mSelection = nullptr;
|
||||
wl_data_source* mSource = nullptr;
|
||||
std::string mOwnedText;
|
||||
Transfer mText;
|
||||
Transfer mImage;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
#include "WaylandConnection.hpp"
|
||||
|
||||
#include "cursor-shape-v1-client-protocol.h"
|
||||
#include "pointer-gestures-unstable-v1-client-protocol.h"
|
||||
#include "fractional-scale-v1-client-protocol.h"
|
||||
#include "tablet-v2-client-protocol.h"
|
||||
#include "text-input-unstable-v3-client-protocol.h"
|
||||
#include "viewporter-client-protocol.h"
|
||||
#include "xdg-activation-v1-client-protocol.h"
|
||||
#include "xdg-decoration-unstable-v1-client-protocol.h"
|
||||
#include "xdg-shell-client-protocol.h"
|
||||
|
||||
#include <wayland-client.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <poll.h>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace tp {
|
||||
|
||||
struct WaylandRegistryListener {
|
||||
static void global(void* data, wl_registry* registry, uint32_t name, const char* interface, uint32_t version) {
|
||||
static_cast<WaylandConnection*>(data)->bindGlobal(registry, name, interface, version);
|
||||
}
|
||||
|
||||
static void globalRemove(void*, wl_registry*, uint32_t) {}
|
||||
|
||||
static void ping(void*, xdg_wm_base* windowManager, uint32_t serial) { xdg_wm_base_pong(windowManager, serial); }
|
||||
};
|
||||
|
||||
namespace {
|
||||
const wl_registry_listener kRegistryListener = {
|
||||
WaylandRegistryListener::global,
|
||||
WaylandRegistryListener::globalRemove,
|
||||
};
|
||||
|
||||
const xdg_wm_base_listener kWindowManagerListener = { WaylandRegistryListener::ping };
|
||||
|
||||
template <typename Interface>
|
||||
Interface*
|
||||
bindAs(wl_registry* registry, uint32_t name, const wl_interface* interface, uint32_t offered, uint32_t wanted) {
|
||||
return static_cast<Interface*>(wl_registry_bind(registry, name, interface, std::min(offered, wanted)));
|
||||
}
|
||||
}
|
||||
|
||||
WaylandConnection::WaylandConnection() {
|
||||
mDisplay = wl_display_connect(nullptr);
|
||||
if (!mDisplay) {
|
||||
throw std::runtime_error("cannot connect to the Wayland display; this build supports Wayland only");
|
||||
}
|
||||
|
||||
mRegistry = wl_display_get_registry(mDisplay);
|
||||
wl_registry_add_listener(mRegistry, &kRegistryListener, this);
|
||||
wl_display_roundtrip(mDisplay);
|
||||
wl_display_roundtrip(mDisplay);
|
||||
|
||||
if (!mCompositor || !mWindowManager) {
|
||||
throw std::runtime_error("Wayland compositor lacks wl_compositor or xdg_wm_base");
|
||||
}
|
||||
}
|
||||
|
||||
WaylandConnection::~WaylandConnection() {
|
||||
if (mDecorationManager) {
|
||||
zxdg_decoration_manager_v1_destroy(mDecorationManager);
|
||||
}
|
||||
if (mTextInputManager) {
|
||||
zwp_text_input_manager_v3_destroy(mTextInputManager);
|
||||
}
|
||||
if (mActivation) {
|
||||
xdg_activation_v1_destroy(mActivation);
|
||||
}
|
||||
if (mTabletManager) {
|
||||
zwp_tablet_manager_v2_destroy(mTabletManager);
|
||||
}
|
||||
if (mCursorShapeManager) {
|
||||
wp_cursor_shape_manager_v1_destroy(mCursorShapeManager);
|
||||
}
|
||||
if (mPointerGestures) {
|
||||
zwp_pointer_gestures_v1_destroy(mPointerGestures);
|
||||
}
|
||||
if (mFractionalScaleManager) {
|
||||
wp_fractional_scale_manager_v1_destroy(mFractionalScaleManager);
|
||||
}
|
||||
if (mViewporter) {
|
||||
wp_viewporter_destroy(mViewporter);
|
||||
}
|
||||
if (mDataDeviceManager) {
|
||||
wl_data_device_manager_destroy(mDataDeviceManager);
|
||||
}
|
||||
if (mSeat) {
|
||||
wl_seat_release(mSeat);
|
||||
}
|
||||
if (mWindowManager) {
|
||||
xdg_wm_base_destroy(mWindowManager);
|
||||
}
|
||||
if (mCompositor) {
|
||||
wl_compositor_destroy(mCompositor);
|
||||
}
|
||||
if (mRegistry) {
|
||||
wl_registry_destroy(mRegistry);
|
||||
}
|
||||
if (mDisplay) {
|
||||
wl_display_flush(mDisplay);
|
||||
wl_display_disconnect(mDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
WaylandConnection::bindGlobal(wl_registry* registry, uint32_t name, const std::string& interface, uint32_t version) {
|
||||
if (interface == wl_compositor_interface.name) {
|
||||
mCompositor = bindAs<wl_compositor>(registry, name, &wl_compositor_interface, version, 4);
|
||||
} else if (interface == xdg_wm_base_interface.name) {
|
||||
mWindowManager = bindAs<xdg_wm_base>(registry, name, &xdg_wm_base_interface, version, 5);
|
||||
xdg_wm_base_add_listener(mWindowManager, &kWindowManagerListener, this);
|
||||
} else if (interface == wl_seat_interface.name && mSeatName == 0) {
|
||||
mSeatName = name;
|
||||
mSeatVersion = version;
|
||||
} else if (interface == wl_data_device_manager_interface.name) {
|
||||
mDataDeviceManager =
|
||||
bindAs<wl_data_device_manager>(registry, name, &wl_data_device_manager_interface, version, 3);
|
||||
} else if (interface == wp_viewporter_interface.name) {
|
||||
mViewporter = bindAs<wp_viewporter>(registry, name, &wp_viewporter_interface, version, 1);
|
||||
} else if (interface == wp_fractional_scale_manager_v1_interface.name) {
|
||||
mFractionalScaleManager =
|
||||
bindAs<wp_fractional_scale_manager_v1>(registry, name, &wp_fractional_scale_manager_v1_interface, version, 1);
|
||||
} else if (interface == zwp_pointer_gestures_v1_interface.name) {
|
||||
mPointerGestures =
|
||||
bindAs<zwp_pointer_gestures_v1>(registry, name, &zwp_pointer_gestures_v1_interface, version, 1);
|
||||
} else if (interface == wp_cursor_shape_manager_v1_interface.name) {
|
||||
mCursorShapeManager =
|
||||
bindAs<wp_cursor_shape_manager_v1>(registry, name, &wp_cursor_shape_manager_v1_interface, version, 1);
|
||||
} else if (interface == zwp_text_input_manager_v3_interface.name) {
|
||||
mTextInputManager =
|
||||
bindAs<zwp_text_input_manager_v3>(registry, name, &zwp_text_input_manager_v3_interface, version, 1);
|
||||
} else if (interface == zxdg_decoration_manager_v1_interface.name) {
|
||||
mDecorationManager =
|
||||
bindAs<zxdg_decoration_manager_v1>(registry, name, &zxdg_decoration_manager_v1_interface, version, 1);
|
||||
} else if (interface == zwp_tablet_manager_v2_interface.name) {
|
||||
mTabletManager = bindAs<zwp_tablet_manager_v2>(registry, name, &zwp_tablet_manager_v2_interface, version, 1);
|
||||
} else if (interface == xdg_activation_v1_interface.name) {
|
||||
mActivation = bindAs<xdg_activation_v1>(registry, name, &xdg_activation_v1_interface, version, 1);
|
||||
}
|
||||
}
|
||||
|
||||
wl_seat* WaylandConnection::bindSeat(const void* listener, void* data) {
|
||||
if (mSeat || mSeatName == 0) {
|
||||
return mSeat;
|
||||
}
|
||||
|
||||
mSeat = bindAs<wl_seat>(mRegistry, mSeatName, &wl_seat_interface, mSeatVersion, 8);
|
||||
wl_seat_add_listener(mSeat, static_cast<const wl_seat_listener*>(listener), data);
|
||||
return mSeat;
|
||||
}
|
||||
|
||||
bool WaylandConnection::dispatch(int timeoutMs) {
|
||||
if (wl_display_dispatch_pending(mDisplay) > 0) {
|
||||
flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
while (wl_display_prepare_read(mDisplay) != 0) {
|
||||
if (wl_display_dispatch_pending(mDisplay) > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
flush();
|
||||
|
||||
pollfd descriptor = { wl_display_get_fd(mDisplay), POLLIN, 0 };
|
||||
const int ready = poll(&descriptor, 1, timeoutMs);
|
||||
if (ready <= 0) {
|
||||
wl_display_cancel_read(mDisplay);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wl_display_read_events(mDisplay) < 0) {
|
||||
throw std::runtime_error("lost the Wayland connection");
|
||||
}
|
||||
wl_display_dispatch_pending(mDisplay);
|
||||
flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
void WaylandConnection::roundtrip() { wl_display_roundtrip(mDisplay); }
|
||||
|
||||
void WaylandConnection::flush() {
|
||||
while (wl_display_flush(mDisplay) < 0 && errno == EAGAIN) {
|
||||
pollfd descriptor = { wl_display_get_fd(mDisplay), POLLOUT, 0 };
|
||||
poll(&descriptor, 1, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
struct wl_display;
|
||||
struct wl_registry;
|
||||
struct wl_compositor;
|
||||
struct wl_seat;
|
||||
struct wl_data_device_manager;
|
||||
struct xdg_wm_base;
|
||||
struct wp_viewporter;
|
||||
struct wp_fractional_scale_manager_v1;
|
||||
struct wp_cursor_shape_manager_v1;
|
||||
struct zwp_pointer_gestures_v1;
|
||||
struct zwp_text_input_manager_v3;
|
||||
struct zxdg_decoration_manager_v1;
|
||||
struct zwp_tablet_manager_v2;
|
||||
struct xdg_activation_v1;
|
||||
|
||||
namespace tp {
|
||||
|
||||
class WaylandConnection {
|
||||
public:
|
||||
WaylandConnection();
|
||||
~WaylandConnection();
|
||||
|
||||
WaylandConnection(const WaylandConnection&) = delete;
|
||||
WaylandConnection& operator=(const WaylandConnection&) = delete;
|
||||
|
||||
[[nodiscard]] wl_display* display() const { return mDisplay; }
|
||||
[[nodiscard]] wl_compositor* compositor() const { return mCompositor; }
|
||||
[[nodiscard]] xdg_wm_base* windowManager() const { return mWindowManager; }
|
||||
[[nodiscard]] wl_seat* seat() const { return mSeat; }
|
||||
[[nodiscard]] wl_seat* bindSeat(const void* listener, void* data);
|
||||
[[nodiscard]] wl_data_device_manager* dataDeviceManager() const { return mDataDeviceManager; }
|
||||
[[nodiscard]] wp_viewporter* viewporter() const { return mViewporter; }
|
||||
[[nodiscard]] wp_fractional_scale_manager_v1* fractionalScaleManager() const { return mFractionalScaleManager; }
|
||||
[[nodiscard]] wp_cursor_shape_manager_v1* cursorShapeManager() const { return mCursorShapeManager; }
|
||||
[[nodiscard]] zwp_pointer_gestures_v1* pointerGestures() const { return mPointerGestures; }
|
||||
[[nodiscard]] zwp_text_input_manager_v3* textInputManager() const { return mTextInputManager; }
|
||||
[[nodiscard]] zxdg_decoration_manager_v1* decorationManager() const { return mDecorationManager; }
|
||||
[[nodiscard]] zwp_tablet_manager_v2* tabletManager() const { return mTabletManager; }
|
||||
[[nodiscard]] xdg_activation_v1* activation() const { return mActivation; }
|
||||
|
||||
bool dispatch(int timeoutMs);
|
||||
void roundtrip();
|
||||
void flush();
|
||||
|
||||
private:
|
||||
friend struct WaylandRegistryListener;
|
||||
|
||||
void bindGlobal(wl_registry* registry, uint32_t name, const std::string& interface, uint32_t version);
|
||||
|
||||
wl_display* mDisplay = nullptr;
|
||||
wl_registry* mRegistry = nullptr;
|
||||
wl_compositor* mCompositor = nullptr;
|
||||
xdg_wm_base* mWindowManager = nullptr;
|
||||
wl_seat* mSeat = nullptr;
|
||||
uint32_t mSeatName = 0;
|
||||
uint32_t mSeatVersion = 0;
|
||||
wl_data_device_manager* mDataDeviceManager = nullptr;
|
||||
wp_viewporter* mViewporter = nullptr;
|
||||
wp_fractional_scale_manager_v1* mFractionalScaleManager = nullptr;
|
||||
wp_cursor_shape_manager_v1* mCursorShapeManager = nullptr;
|
||||
zwp_pointer_gestures_v1* mPointerGestures = nullptr;
|
||||
zwp_text_input_manager_v3* mTextInputManager = nullptr;
|
||||
zxdg_decoration_manager_v1* mDecorationManager = nullptr;
|
||||
zwp_tablet_manager_v2* mTabletManager = nullptr;
|
||||
xdg_activation_v1* mActivation = nullptr;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
#include "WaylandKeyboard.hpp"
|
||||
|
||||
#include "LinuxInputCodes.hpp"
|
||||
|
||||
#include <wayland-client.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/timerfd.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
WaylandKeyboard& keyboard(void* data) { return *static_cast<WaylandKeyboard*>(data); }
|
||||
|
||||
void keymap(void* data, wl_keyboard*, uint32_t format, int32_t descriptor, uint32_t size) {
|
||||
keyboard(data).onKeymap(format, descriptor, size);
|
||||
}
|
||||
void keyEnter(void* data, wl_keyboard*, uint32_t serial, wl_surface*, wl_array* keys) {
|
||||
keyboard(data).onEnter(serial, keys);
|
||||
}
|
||||
void keyLeave(void* data, wl_keyboard*, uint32_t, wl_surface*) { keyboard(data).onLeave(); }
|
||||
void key(void* data, wl_keyboard*, uint32_t serial, uint32_t, uint32_t key, uint32_t state) {
|
||||
keyboard(data).onKey(serial, key, state);
|
||||
}
|
||||
void modifiers(
|
||||
void* data, wl_keyboard*, uint32_t, uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group
|
||||
) {
|
||||
keyboard(data).onModifiers(depressed, latched, locked, group);
|
||||
}
|
||||
void repeatInfo(void* data, wl_keyboard*, int32_t rate, int32_t delay) { keyboard(data).onRepeatInfo(rate, delay); }
|
||||
|
||||
const wl_keyboard_listener kKeyboardListener = { keymap, keyEnter, keyLeave, key, modifiers, repeatInfo };
|
||||
|
||||
timespec millisecondsToTimespec(int32_t milliseconds) {
|
||||
return { milliseconds / 1000, (long) (milliseconds % 1000) * 1000000L };
|
||||
}
|
||||
}
|
||||
|
||||
WaylandKeyboard::WaylandKeyboard(WaylandSeatContext& seat, wl_keyboard* keyboard) :
|
||||
mSeat(seat),
|
||||
mKeyboard(keyboard) {
|
||||
mContext = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
||||
mRepeatTimer = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
|
||||
|
||||
const char* locale = std::getenv("LC_ALL");
|
||||
if (!locale || !*locale) {
|
||||
locale = std::getenv("LC_CTYPE");
|
||||
}
|
||||
if (!locale || !*locale) {
|
||||
locale = std::getenv("LANG");
|
||||
}
|
||||
if (!locale || !*locale) {
|
||||
locale = "C";
|
||||
}
|
||||
mComposeTable = xkb_compose_table_new_from_locale(mContext, locale, XKB_COMPOSE_COMPILE_NO_FLAGS);
|
||||
if (mComposeTable) {
|
||||
mComposeState = xkb_compose_state_new(mComposeTable, XKB_COMPOSE_STATE_NO_FLAGS);
|
||||
}
|
||||
|
||||
wl_keyboard_add_listener(mKeyboard, &kKeyboardListener, this);
|
||||
}
|
||||
|
||||
WaylandKeyboard::~WaylandKeyboard() {
|
||||
if (mRepeatTimer >= 0) {
|
||||
close(mRepeatTimer);
|
||||
}
|
||||
if (mComposeState) {
|
||||
xkb_compose_state_unref(mComposeState);
|
||||
}
|
||||
if (mComposeTable) {
|
||||
xkb_compose_table_unref(mComposeTable);
|
||||
}
|
||||
if (mState) {
|
||||
xkb_state_unref(mState);
|
||||
}
|
||||
if (mKeymap) {
|
||||
xkb_keymap_unref(mKeymap);
|
||||
}
|
||||
if (mContext) {
|
||||
xkb_context_unref(mContext);
|
||||
}
|
||||
wl_keyboard_release(mKeyboard);
|
||||
}
|
||||
|
||||
void WaylandKeyboard::onKeymap(uint32_t format, int descriptor, uint32_t size) {
|
||||
if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
|
||||
close(descriptor);
|
||||
return;
|
||||
}
|
||||
|
||||
void* mapped = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, descriptor, 0);
|
||||
close(descriptor);
|
||||
if (mapped == MAP_FAILED) {
|
||||
return;
|
||||
}
|
||||
|
||||
xkb_keymap* keymap = xkb_keymap_new_from_string(
|
||||
mContext, static_cast<const char*>(mapped), XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS
|
||||
);
|
||||
munmap(mapped, size);
|
||||
if (!keymap) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mState) {
|
||||
xkb_state_unref(mState);
|
||||
}
|
||||
if (mKeymap) {
|
||||
xkb_keymap_unref(mKeymap);
|
||||
}
|
||||
mKeymap = keymap;
|
||||
mState = xkb_state_new(mKeymap);
|
||||
}
|
||||
|
||||
void WaylandKeyboard::onEnter(uint32_t serial, wl_array* keys) {
|
||||
mSeat.recordSerial(serial);
|
||||
mSeat.input().post(KeyboardFocusChanged{ true });
|
||||
|
||||
const auto* pressed = static_cast<const uint32_t*>(keys->data);
|
||||
const size_t count = keys->size / sizeof(uint32_t);
|
||||
for (size_t index = 0; index < count; index++) {
|
||||
if (const std::optional<Key> key = keyFromEvdev(pressed[index])) {
|
||||
mSeat.input().post(KeyChanged{ *key, KeyAction::Press });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandKeyboard::onLeave() {
|
||||
disarmRepeat();
|
||||
if (mComposeState) {
|
||||
xkb_compose_state_reset(mComposeState);
|
||||
}
|
||||
mSeat.input().post(KeyboardFocusChanged{ false });
|
||||
}
|
||||
|
||||
void WaylandKeyboard::onKey(uint32_t serial, uint32_t key, uint32_t state) {
|
||||
mSeat.recordSerial(serial);
|
||||
|
||||
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
||||
pressKey(key);
|
||||
} else {
|
||||
releaseKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandKeyboard::pressKey(uint32_t key) {
|
||||
if (const std::optional<Key> id = keyFromEvdev(key)) {
|
||||
mSeat.input().post(KeyChanged{ *id, KeyAction::Press });
|
||||
}
|
||||
|
||||
emitText(key);
|
||||
|
||||
if (mKeymap && xkb_keymap_key_repeats(mKeymap, key + 8)) {
|
||||
armRepeat(key);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandKeyboard::releaseKey(uint32_t key) {
|
||||
if (key == mRepeatKey) {
|
||||
disarmRepeat();
|
||||
}
|
||||
|
||||
if (const std::optional<Key> id = keyFromEvdev(key)) {
|
||||
mSeat.input().post(KeyChanged{ *id, KeyAction::Release });
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandKeyboard::emitText(uint32_t key) {
|
||||
if (!mState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const xkb_keysym_t symbol = composed(xkb_state_key_get_one_sym(mState, key + 8));
|
||||
if (symbol == XKB_KEY_NoSymbol) {
|
||||
return;
|
||||
}
|
||||
|
||||
char utf8[64] = {};
|
||||
if (mComposeState && xkb_compose_state_get_status(mComposeState) == XKB_COMPOSE_COMPOSED) {
|
||||
xkb_compose_state_get_utf8(mComposeState, utf8, sizeof(utf8));
|
||||
xkb_compose_state_reset(mComposeState);
|
||||
} else {
|
||||
xkb_state_key_get_utf8(mState, key + 8, utf8, sizeof(utf8));
|
||||
}
|
||||
|
||||
const unsigned char first = (unsigned char) utf8[0];
|
||||
if (first == 0 || first < 0x20 || first == 0x7f) {
|
||||
return;
|
||||
}
|
||||
mSeat.input().post(TextEntered{ utf8 });
|
||||
}
|
||||
|
||||
xkb_keysym_t WaylandKeyboard::composed(xkb_keysym_t symbol) {
|
||||
if (!mComposeState || symbol == XKB_KEY_NoSymbol) {
|
||||
return symbol;
|
||||
}
|
||||
if (xkb_compose_state_feed(mComposeState, symbol) != XKB_COMPOSE_FEED_ACCEPTED) {
|
||||
return symbol;
|
||||
}
|
||||
|
||||
switch (xkb_compose_state_get_status(mComposeState)) {
|
||||
case XKB_COMPOSE_COMPOSING: return XKB_KEY_NoSymbol;
|
||||
case XKB_COMPOSE_CANCELLED: xkb_compose_state_reset(mComposeState); return XKB_KEY_NoSymbol;
|
||||
case XKB_COMPOSE_COMPOSED: return xkb_compose_state_get_one_sym(mComposeState);
|
||||
case XKB_COMPOSE_NOTHING: return symbol;
|
||||
}
|
||||
return symbol;
|
||||
}
|
||||
|
||||
void WaylandKeyboard::onModifiers(uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group) {
|
||||
if (mState) {
|
||||
xkb_state_update_mask(mState, depressed, latched, locked, 0, 0, group);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandKeyboard::onRepeatInfo(int32_t rate, int32_t delay) {
|
||||
mRepeatRate = rate;
|
||||
mRepeatDelay = delay;
|
||||
}
|
||||
|
||||
void WaylandKeyboard::armRepeat(uint32_t key) {
|
||||
if (mRepeatTimer < 0 || mRepeatRate <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
mRepeatKey = key;
|
||||
itimerspec timer = {};
|
||||
timer.it_value = millisecondsToTimespec(mRepeatDelay);
|
||||
timer.it_interval = millisecondsToTimespec(1000 / mRepeatRate);
|
||||
timerfd_settime(mRepeatTimer, 0, &timer, nullptr);
|
||||
}
|
||||
|
||||
void WaylandKeyboard::disarmRepeat() {
|
||||
mRepeatKey = 0;
|
||||
if (mRepeatTimer < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
itimerspec timer = {};
|
||||
timerfd_settime(mRepeatTimer, 0, &timer, nullptr);
|
||||
}
|
||||
|
||||
void WaylandKeyboard::onRepeatTimer() {
|
||||
uint64_t expirations = 0;
|
||||
if (read(mRepeatTimer, &expirations, sizeof(expirations)) != sizeof(expirations)) {
|
||||
return;
|
||||
}
|
||||
if (mRepeatKey == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::optional<Key> id = keyFromEvdev(mRepeatKey);
|
||||
for (uint64_t repeat = 0; repeat < expirations; repeat++) {
|
||||
if (id) {
|
||||
mSeat.input().post(KeyChanged{ *id, KeyAction::Repeat });
|
||||
}
|
||||
emitText(mRepeatKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
#pragma once
|
||||
|
||||
#include "WaylandSeatContext.hpp"
|
||||
|
||||
#include <xkbcommon/xkbcommon-compose.h>
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
struct wl_keyboard;
|
||||
struct wl_array;
|
||||
|
||||
namespace tp {
|
||||
|
||||
// wl_keyboard with xkb keymap, compose sequences and client-side key repeat.
|
||||
class WaylandKeyboard {
|
||||
public:
|
||||
WaylandKeyboard(WaylandSeatContext& seat, wl_keyboard* keyboard);
|
||||
~WaylandKeyboard();
|
||||
|
||||
WaylandKeyboard(const WaylandKeyboard&) = delete;
|
||||
WaylandKeyboard& operator=(const WaylandKeyboard&) = delete;
|
||||
|
||||
[[nodiscard]] int repeatTimerDescriptor() const { return mRepeatTimer; }
|
||||
void onRepeatTimer();
|
||||
|
||||
void onKeymap(uint32_t format, int descriptor, uint32_t size);
|
||||
void onEnter(uint32_t serial, wl_array* keys);
|
||||
void onLeave();
|
||||
void onKey(uint32_t serial, uint32_t key, uint32_t state);
|
||||
void onModifiers(uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group);
|
||||
void onRepeatInfo(int32_t rate, int32_t delay);
|
||||
|
||||
private:
|
||||
void pressKey(uint32_t key);
|
||||
void releaseKey(uint32_t key);
|
||||
void emitText(uint32_t key);
|
||||
void armRepeat(uint32_t key);
|
||||
void disarmRepeat();
|
||||
[[nodiscard]] xkb_keysym_t composed(xkb_keysym_t symbol);
|
||||
|
||||
WaylandSeatContext& mSeat;
|
||||
wl_keyboard* mKeyboard;
|
||||
xkb_context* mContext = nullptr;
|
||||
xkb_keymap* mKeymap = nullptr;
|
||||
xkb_state* mState = nullptr;
|
||||
xkb_compose_table* mComposeTable = nullptr;
|
||||
xkb_compose_state* mComposeState = nullptr;
|
||||
int mRepeatTimer = -1;
|
||||
int32_t mRepeatRate = 25;
|
||||
int32_t mRepeatDelay = 600;
|
||||
uint32_t mRepeatKey = 0;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
#include "WaylandPointer.hpp"
|
||||
|
||||
#include "WaylandConnection.hpp"
|
||||
|
||||
#include "cursor-shape-v1-client-protocol.h"
|
||||
#include "pointer-gestures-unstable-v1-client-protocol.h"
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
constexpr halnf kWheelStep = 10;
|
||||
|
||||
WaylandPointer& pointer(void* data) { return *static_cast<WaylandPointer*>(data); }
|
||||
|
||||
void pointerEnter(void* data, wl_pointer*, uint32_t serial, wl_surface*, wl_fixed_t x, wl_fixed_t y) {
|
||||
pointer(data).onEnter(serial, x, y);
|
||||
}
|
||||
void pointerLeave(void* data, wl_pointer*, uint32_t, wl_surface*) { pointer(data).onLeave(); }
|
||||
void pointerMotion(void* data, wl_pointer*, uint32_t, wl_fixed_t x, wl_fixed_t y) { pointer(data).onMotion(x, y); }
|
||||
void pointerButton(void* data, wl_pointer*, uint32_t serial, uint32_t, uint32_t button, uint32_t state) {
|
||||
pointer(data).onButton(serial, button, state);
|
||||
}
|
||||
void pointerAxis(void* data, wl_pointer*, uint32_t, uint32_t axis, wl_fixed_t value) {
|
||||
pointer(data).onAxis(axis, value);
|
||||
}
|
||||
void pointerFrame(void* data, wl_pointer*) { pointer(data).onFrame(); }
|
||||
void pointerAxisSource(void* data, wl_pointer*, uint32_t source) { pointer(data).onAxisSource(source); }
|
||||
void pointerAxisStop(void* data, wl_pointer*, uint32_t, uint32_t) { pointer(data).onAxisStop(); }
|
||||
void pointerAxisDiscrete(void* data, wl_pointer*, uint32_t axis, int32_t steps) {
|
||||
pointer(data).onAxisDiscrete(axis, steps);
|
||||
}
|
||||
void pointerAxisValue120(void*, wl_pointer*, uint32_t, int32_t) {}
|
||||
void pointerAxisRelativeDirection(void*, wl_pointer*, uint32_t, uint32_t) {}
|
||||
|
||||
const wl_pointer_listener kPointerListener = {
|
||||
pointerEnter,
|
||||
pointerLeave,
|
||||
pointerMotion,
|
||||
pointerButton,
|
||||
pointerAxis,
|
||||
pointerFrame,
|
||||
pointerAxisSource,
|
||||
pointerAxisStop,
|
||||
pointerAxisDiscrete,
|
||||
pointerAxisValue120,
|
||||
pointerAxisRelativeDirection,
|
||||
};
|
||||
|
||||
void pinchBegin(void* data, zwp_pointer_gesture_pinch_v1*, uint32_t, uint32_t, wl_surface*, uint32_t) {
|
||||
pointer(data).onPinchBegin();
|
||||
}
|
||||
void pinchUpdate(
|
||||
void* data,
|
||||
zwp_pointer_gesture_pinch_v1*,
|
||||
uint32_t,
|
||||
wl_fixed_t dx,
|
||||
wl_fixed_t dy,
|
||||
wl_fixed_t scale,
|
||||
wl_fixed_t rotation
|
||||
) {
|
||||
pointer(data).onPinchUpdate(dx, dy, scale, rotation);
|
||||
}
|
||||
void pinchEnd(void* data, zwp_pointer_gesture_pinch_v1*, uint32_t, uint32_t, int32_t) {
|
||||
pointer(data).onPinchEnd();
|
||||
}
|
||||
|
||||
const zwp_pointer_gesture_pinch_v1_listener kPinchListener = { pinchBegin, pinchUpdate, pinchEnd };
|
||||
}
|
||||
|
||||
uint32_t cursorShapeCode(CursorShape shape) {
|
||||
switch (shape) {
|
||||
case CursorShape::Default: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT;
|
||||
case CursorShape::Pointer: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER;
|
||||
case CursorShape::Text: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT;
|
||||
case CursorShape::Move: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE;
|
||||
case CursorShape::Grab: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRAB;
|
||||
case CursorShape::Grabbing: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRABBING;
|
||||
case CursorShape::ResizeHorizontal: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE;
|
||||
case CursorShape::ResizeVertical: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE;
|
||||
case CursorShape::ResizeNorthWestSouthEast: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NWSE_RESIZE;
|
||||
case CursorShape::ResizeNorthEastSouthWest: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NESW_RESIZE;
|
||||
case CursorShape::NotAllowed: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NOT_ALLOWED;
|
||||
case CursorShape::Crosshair: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR;
|
||||
case CursorShape::Hidden: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT;
|
||||
}
|
||||
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT;
|
||||
}
|
||||
|
||||
WaylandPointer::WaylandPointer(WaylandSeatContext& seat, wl_pointer* pointer) :
|
||||
mSeat(seat),
|
||||
mPointer(pointer) {
|
||||
wl_pointer_add_listener(mPointer, &kPointerListener, this);
|
||||
|
||||
wp_cursor_shape_manager_v1* manager = seat.connection().cursorShapeManager();
|
||||
if (manager) {
|
||||
mShapeDevice = wp_cursor_shape_manager_v1_get_pointer(manager, mPointer);
|
||||
}
|
||||
|
||||
zwp_pointer_gestures_v1* gestures = seat.connection().pointerGestures();
|
||||
if (gestures) {
|
||||
mPinch = zwp_pointer_gestures_v1_get_pinch_gesture(gestures, mPointer);
|
||||
zwp_pointer_gesture_pinch_v1_add_listener(mPinch, &kPinchListener, this);
|
||||
}
|
||||
}
|
||||
|
||||
WaylandPointer::~WaylandPointer() {
|
||||
if (mPinch) {
|
||||
zwp_pointer_gesture_pinch_v1_destroy(mPinch);
|
||||
}
|
||||
if (mShapeDevice) {
|
||||
wp_cursor_shape_device_v1_destroy(mShapeDevice);
|
||||
}
|
||||
wl_pointer_release(mPointer);
|
||||
}
|
||||
|
||||
void WaylandPointer::onPinchBegin() {
|
||||
mSeat.surfacePointer().onPointerSurfacePinch(mPinchConverter.begin(mSurfacePosition));
|
||||
}
|
||||
|
||||
void WaylandPointer::onPinchUpdate(wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale, wl_fixed_t rotation) {
|
||||
const Vec2F pan = { (halnf) wl_fixed_to_double(dx), (halnf) wl_fixed_to_double(dy) };
|
||||
const std::optional<TouchGesture> gesture = mPinchConverter.update(
|
||||
mSurfacePosition, pan, (halnf) wl_fixed_to_double(scale), (halnf) wl_fixed_to_double(rotation)
|
||||
);
|
||||
if (gesture) {
|
||||
mSeat.surfacePointer().onPointerSurfacePinch(*gesture);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandPointer::onPinchEnd() {
|
||||
if (const std::optional<TouchGesture> gesture = mPinchConverter.end(mSurfacePosition)) {
|
||||
mSeat.surfacePointer().onPointerSurfacePinch(*gesture);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandPointer::onEnter(uint32_t serial, wl_fixed_t x, wl_fixed_t y) {
|
||||
mEnterSerial = serial;
|
||||
mInside = true;
|
||||
mSurfacePosition = { (halnf) wl_fixed_to_double(x), (halnf) wl_fixed_to_double(y) };
|
||||
mShapeApplied = false;
|
||||
mSeat.surfacePointer().onPointerSurfaceMove({ (halnf) wl_fixed_to_double(x), (halnf) wl_fixed_to_double(y) });
|
||||
applyCursor();
|
||||
}
|
||||
|
||||
void WaylandPointer::onLeave() {
|
||||
mInside = false;
|
||||
mSeat.surfacePointer().onPointerSurfaceLeft();
|
||||
}
|
||||
|
||||
void WaylandPointer::onMotion(wl_fixed_t x, wl_fixed_t y) {
|
||||
mSurfacePosition = { (halnf) wl_fixed_to_double(x), (halnf) wl_fixed_to_double(y) };
|
||||
mSeat.input().post(PointerSampled{});
|
||||
mSeat.surfacePointer().onPointerSurfaceMove(mSurfacePosition);
|
||||
}
|
||||
|
||||
void WaylandPointer::onButton(uint32_t serial, uint32_t button, uint32_t state) {
|
||||
mSeat.recordSerial(serial);
|
||||
mSeat.input().post(PointerSampled{});
|
||||
mSeat.surfacePointer().onPointerSurfaceButton(button, state == WL_POINTER_BUTTON_STATE_PRESSED, serial);
|
||||
}
|
||||
|
||||
void WaylandPointer::onAxis(uint32_t axis, wl_fixed_t value) {
|
||||
if (mDiscreteScroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
const halnf amount = (halnf) -wl_fixed_to_double(value) / kWheelStep;
|
||||
if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) {
|
||||
mPendingScroll.y += amount;
|
||||
} else {
|
||||
mPendingScroll.x += amount;
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandPointer::onAxisDiscrete(uint32_t axis, int32_t steps) {
|
||||
mDiscreteScroll = true;
|
||||
if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) {
|
||||
mPendingScroll.y -= (halnf) steps;
|
||||
} else {
|
||||
mPendingScroll.x -= (halnf) steps;
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandPointer::onAxisSource(uint32_t source) {
|
||||
const bool continuous = source == WL_POINTER_AXIS_SOURCE_FINGER || source == WL_POINTER_AXIS_SOURCE_CONTINUOUS;
|
||||
mScrollSource = continuous ? ScrollSource::Touchpad : ScrollSource::Wheel;
|
||||
}
|
||||
|
||||
void WaylandPointer::onAxisStop() { mScrollStopped = true; }
|
||||
|
||||
void WaylandPointer::onFrame() {
|
||||
InputSink& input = mSeat.input();
|
||||
if (mPendingScroll.x != 0 || mPendingScroll.y != 0) {
|
||||
input.post(Scrolled{ mPendingScroll, mScrollSource });
|
||||
}
|
||||
if (mScrollStopped) {
|
||||
input.post(ScrollEnded{});
|
||||
}
|
||||
mPendingScroll = { 0, 0 };
|
||||
mDiscreteScroll = false;
|
||||
mScrollStopped = false;
|
||||
mScrollSource = ScrollSource::Wheel;
|
||||
}
|
||||
|
||||
void WaylandPointer::setCursor(CursorShape shape) {
|
||||
if (shape == mShape && mShapeApplied) {
|
||||
return;
|
||||
}
|
||||
|
||||
mShape = shape;
|
||||
mShapeApplied = false;
|
||||
applyCursor();
|
||||
}
|
||||
|
||||
void WaylandPointer::applyCursor() {
|
||||
if (!mInside || mShapeApplied) {
|
||||
return;
|
||||
}
|
||||
if (mShape == CursorShape::Hidden) {
|
||||
wl_pointer_set_cursor(mPointer, mEnterSerial, nullptr, 0, 0);
|
||||
mShapeApplied = true;
|
||||
return;
|
||||
}
|
||||
if (!mShapeDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_cursor_shape_device_v1_set_shape(mShapeDevice, mEnterSerial, cursorShapeCode(mShape));
|
||||
mShapeApplied = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
#pragma once
|
||||
|
||||
#include "PointerPinchConverter.hpp"
|
||||
#include "WaylandSeatContext.hpp"
|
||||
#include "WindowTypes.hpp"
|
||||
|
||||
#include <wayland-client.h>
|
||||
|
||||
struct wp_cursor_shape_device_v1;
|
||||
struct zwp_pointer_gesture_pinch_v1;
|
||||
|
||||
namespace tp {
|
||||
|
||||
// wl_pointer with cursor shapes, frame-grouped scrolling and touchpad pinches.
|
||||
class WaylandPointer {
|
||||
public:
|
||||
WaylandPointer(WaylandSeatContext& seat, wl_pointer* pointer);
|
||||
~WaylandPointer();
|
||||
|
||||
WaylandPointer(const WaylandPointer&) = delete;
|
||||
WaylandPointer& operator=(const WaylandPointer&) = delete;
|
||||
|
||||
void setCursor(CursorShape shape);
|
||||
|
||||
void onEnter(uint32_t serial, wl_fixed_t x, wl_fixed_t y);
|
||||
void onLeave();
|
||||
void onMotion(wl_fixed_t x, wl_fixed_t y);
|
||||
void onButton(uint32_t serial, uint32_t button, uint32_t state);
|
||||
void onAxis(uint32_t axis, wl_fixed_t value);
|
||||
void onAxisDiscrete(uint32_t axis, int32_t steps);
|
||||
void onAxisSource(uint32_t source);
|
||||
void onAxisStop();
|
||||
void onFrame();
|
||||
|
||||
void onPinchBegin();
|
||||
void onPinchUpdate(wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale, wl_fixed_t rotation);
|
||||
void onPinchEnd();
|
||||
|
||||
private:
|
||||
void applyCursor();
|
||||
|
||||
WaylandSeatContext& mSeat;
|
||||
wl_pointer* mPointer;
|
||||
zwp_pointer_gesture_pinch_v1* mPinch = nullptr;
|
||||
PointerPinchConverter mPinchConverter;
|
||||
Vec2F mSurfacePosition = { 0, 0 };
|
||||
wp_cursor_shape_device_v1* mShapeDevice = nullptr;
|
||||
uint32_t mEnterSerial = 0;
|
||||
bool mInside = false;
|
||||
CursorShape mShape = CursorShape::Default;
|
||||
bool mShapeApplied = false;
|
||||
Vec2F mPendingScroll = { 0, 0 };
|
||||
bool mDiscreteScroll = false;
|
||||
ScrollSource mScrollSource = ScrollSource::Wheel;
|
||||
bool mScrollStopped = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] uint32_t cursorShapeCode(CursorShape shape);
|
||||
}
|
||||
113
GraphicApplication/src/backends/wayland/private/WaylandSeat.cpp
Normal file
113
GraphicApplication/src/backends/wayland/private/WaylandSeat.cpp
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
#include "WaylandSeat.hpp"
|
||||
|
||||
#include "WaylandClipboard.hpp"
|
||||
#include "WaylandConnection.hpp"
|
||||
#include "WaylandKeyboard.hpp"
|
||||
#include "WaylandPointer.hpp"
|
||||
#include "WaylandTablet.hpp"
|
||||
#include "WaylandTextInput.hpp"
|
||||
|
||||
#include <wayland-client.h>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
void capabilities(void* data, wl_seat*, uint32_t capabilities) {
|
||||
static_cast<WaylandSeat*>(data)->onCapabilities(capabilities);
|
||||
}
|
||||
|
||||
void name(void*, wl_seat*, const char*) {}
|
||||
|
||||
const wl_seat_listener kSeatListener = { capabilities, name };
|
||||
}
|
||||
|
||||
WaylandSeat::WaylandSeat(
|
||||
WaylandConnection& connection, PlatformWindowHost& host, WaylandSurfacePointer& surfacePointer
|
||||
) :
|
||||
mContext(connection, host, surfacePointer) {
|
||||
if (!connection.bindSeat(&kSeatListener, this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (connection.dataDeviceManager()) {
|
||||
mClipboard = std::make_unique<WaylandClipboard>(mContext, connection.dataDeviceManager(), connection.seat());
|
||||
}
|
||||
if (connection.textInputManager()) {
|
||||
mTextInput = std::make_unique<WaylandTextInput>(mContext, connection.textInputManager(), connection.seat());
|
||||
}
|
||||
if (connection.tabletManager()) {
|
||||
mTablet = std::make_unique<WaylandTablet>(mContext, connection.tabletManager(), connection.seat());
|
||||
}
|
||||
}
|
||||
|
||||
WaylandSeat::~WaylandSeat() = default;
|
||||
|
||||
void WaylandSeat::onCapabilities(uint32_t capabilities) {
|
||||
const bool hasKeyboard = capabilities & WL_SEAT_CAPABILITY_KEYBOARD;
|
||||
const bool hasPointer = capabilities & WL_SEAT_CAPABILITY_POINTER;
|
||||
mHasTouch = capabilities & WL_SEAT_CAPABILITY_TOUCH;
|
||||
wl_seat* seat = mContext.connection().seat();
|
||||
|
||||
if (hasKeyboard && !mKeyboard) {
|
||||
mKeyboard = std::make_unique<WaylandKeyboard>(mContext, wl_seat_get_keyboard(seat));
|
||||
}
|
||||
if (!hasKeyboard) {
|
||||
mKeyboard.reset();
|
||||
}
|
||||
|
||||
if (hasPointer && !mPointer) {
|
||||
mPointer = std::make_unique<WaylandPointer>(mContext, wl_seat_get_pointer(seat));
|
||||
}
|
||||
if (!hasPointer) {
|
||||
mPointer.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandSeat::collectPollDescriptors(std::vector<pollfd>& descriptors) const {
|
||||
if (mKeyboard && mKeyboard->repeatTimerDescriptor() >= 0) {
|
||||
descriptors.push_back({ mKeyboard->repeatTimerDescriptor(), POLLIN, 0 });
|
||||
}
|
||||
if (mClipboard) {
|
||||
mClipboard->collectPollDescriptors(descriptors);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandSeat::onDescriptorsReady(const std::vector<pollfd>& descriptors) {
|
||||
for (const pollfd& ready : descriptors) {
|
||||
if (mKeyboard && ready.fd == mKeyboard->repeatTimerDescriptor() && (ready.revents & POLLIN)) {
|
||||
mKeyboard->onRepeatTimer();
|
||||
}
|
||||
}
|
||||
if (mClipboard) {
|
||||
mClipboard->onDescriptorsReady(descriptors);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandSeat::setCursor(CursorShape shape) {
|
||||
if (mPointer) {
|
||||
mPointer->setCursor(shape);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandSeat::setClipboardText(const std::string& text) {
|
||||
if (mClipboard) {
|
||||
mClipboard->setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
std::string WaylandSeat::clipboardText() const { return mClipboard ? mClipboard->text() : std::string(); }
|
||||
|
||||
ClipboardImage WaylandSeat::clipboardImage() const { return mClipboard ? mClipboard->image() : ClipboardImage{}; }
|
||||
|
||||
void WaylandSeat::setTextInputEnabled(bool enabled) {
|
||||
if (mTextInput) {
|
||||
mTextInput->setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandSeat::setTextInputRect(const RectF& surfaceRect) {
|
||||
if (mTextInput) {
|
||||
mTextInput->setCursorRect(surfaceRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
#pragma once
|
||||
|
||||
#include "Rect.hpp"
|
||||
#include "WaylandSeatContext.hpp"
|
||||
#include "WindowTypes.hpp"
|
||||
|
||||
#include <poll.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
class WaylandClipboard;
|
||||
class WaylandKeyboard;
|
||||
class WaylandPointer;
|
||||
class WaylandTablet;
|
||||
class WaylandTextInput;
|
||||
|
||||
// The wl_seat and the input devices it currently offers.
|
||||
class WaylandSeat {
|
||||
public:
|
||||
WaylandSeat(WaylandConnection& connection, PlatformWindowHost& host, WaylandSurfacePointer& surfacePointer);
|
||||
~WaylandSeat();
|
||||
|
||||
WaylandSeat(const WaylandSeat&) = delete;
|
||||
WaylandSeat& operator=(const WaylandSeat&) = delete;
|
||||
|
||||
void collectPollDescriptors(std::vector<pollfd>& descriptors) const;
|
||||
void onDescriptorsReady(const std::vector<pollfd>& descriptors);
|
||||
|
||||
[[nodiscard]] uint32_t lastInputSerial() const { return mContext.lastInputSerial(); }
|
||||
|
||||
void setCursor(CursorShape shape);
|
||||
|
||||
void setClipboardText(const std::string& text);
|
||||
[[nodiscard]] std::string clipboardText() const;
|
||||
[[nodiscard]] ClipboardImage clipboardImage() const;
|
||||
|
||||
void setTextInputEnabled(bool enabled);
|
||||
void setTextInputRect(const RectF& surfaceRect);
|
||||
|
||||
[[nodiscard]] bool hasPointer() const { return mPointer != nullptr; }
|
||||
[[nodiscard]] bool hasKeyboard() const { return mKeyboard != nullptr; }
|
||||
[[nodiscard]] bool hasTouch() const { return mHasTouch; }
|
||||
[[nodiscard]] bool hasTablet() const { return mTablet != nullptr; }
|
||||
|
||||
void onCapabilities(uint32_t capabilities);
|
||||
|
||||
private:
|
||||
WaylandSeatContext mContext;
|
||||
std::unique_ptr<WaylandKeyboard> mKeyboard;
|
||||
std::unique_ptr<WaylandPointer> mPointer;
|
||||
std::unique_ptr<WaylandClipboard> mClipboard;
|
||||
std::unique_ptr<WaylandTextInput> mTextInput;
|
||||
std::unique_ptr<WaylandTablet> mTablet;
|
||||
bool mHasTouch = false;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
|
||||
#include "PlatformWindow.hpp"
|
||||
#include "TouchGesture.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace tp {
|
||||
|
||||
class WaylandConnection;
|
||||
|
||||
// Pointer-like devices report in surface coordinates; the window maps them to content or decoration edges.
|
||||
class WaylandSurfacePointer {
|
||||
public:
|
||||
virtual ~WaylandSurfacePointer() = default;
|
||||
|
||||
virtual void onPointerSurfaceMove(Vec2F surfacePoint) = 0;
|
||||
virtual void onPointerSurfaceButton(uint32_t linuxButton, bool pressed, uint32_t serial) = 0;
|
||||
virtual void onPointerSurfaceLeft() = 0;
|
||||
virtual void onPointerSurfacePinch(const TouchGesture& gesture) = 0;
|
||||
};
|
||||
|
||||
// What every seat device shares: the connection, the raw input entry, surface pointer routing and the last serial.
|
||||
class WaylandSeatContext {
|
||||
public:
|
||||
WaylandSeatContext(WaylandConnection& connection, PlatformWindowHost& host, WaylandSurfacePointer& surfacePointer) :
|
||||
mConnection(connection),
|
||||
mHost(host),
|
||||
mSurfacePointer(surfacePointer) {}
|
||||
|
||||
[[nodiscard]] WaylandConnection& connection() const { return mConnection; }
|
||||
[[nodiscard]] InputSink& input() const { return mHost.rawInput(); }
|
||||
[[nodiscard]] WaylandSurfacePointer& surfacePointer() const { return mSurfacePointer; }
|
||||
|
||||
void recordSerial(uint32_t serial) { mLastInputSerial = serial; }
|
||||
[[nodiscard]] uint32_t lastInputSerial() const { return mLastInputSerial; }
|
||||
|
||||
private:
|
||||
WaylandConnection& mConnection;
|
||||
PlatformWindowHost& mHost;
|
||||
WaylandSurfacePointer& mSurfacePointer;
|
||||
uint32_t mLastInputSerial = 0;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
#include "WaylandTablet.hpp"
|
||||
|
||||
#include "WaylandConnection.hpp"
|
||||
|
||||
#include "cursor-shape-v1-client-protocol.h"
|
||||
#include "tablet-v2-client-protocol.h"
|
||||
|
||||
#include <wayland-client.h>
|
||||
|
||||
#include <linux/input-event-codes.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
WaylandTablet& tablet(void* data) { return *static_cast<WaylandTablet*>(data); }
|
||||
WaylandTablet::Tool& tool(void* data) { return *static_cast<WaylandTablet::Tool*>(data); }
|
||||
|
||||
void tabletAdded(void* data, zwp_tablet_seat_v2*, zwp_tablet_v2* id) { tablet(data).onTabletAdded(id); }
|
||||
void toolAdded(void* data, zwp_tablet_seat_v2*, zwp_tablet_tool_v2* id) { tablet(data).onToolAdded(id); }
|
||||
void padAdded(void* data, zwp_tablet_seat_v2*, zwp_tablet_pad_v2* id) { tablet(data).onPadAdded(id); }
|
||||
|
||||
void tabletName(void*, zwp_tablet_v2*, const char*) {}
|
||||
void tabletId(void*, zwp_tablet_v2*, uint32_t, uint32_t) {}
|
||||
void tabletPath(void*, zwp_tablet_v2*, const char*) {}
|
||||
void tabletDone(void*, zwp_tablet_v2*) {}
|
||||
void tabletRemoved(void*, zwp_tablet_v2*) {}
|
||||
void tabletBustype(void*, zwp_tablet_v2*, uint32_t) {}
|
||||
|
||||
void toolType(void* data, zwp_tablet_tool_v2*, uint32_t type) { tool(data).owner.onToolType(tool(data), type); }
|
||||
void toolSerial(void*, zwp_tablet_tool_v2*, uint32_t, uint32_t) {}
|
||||
void toolWacomId(void*, zwp_tablet_tool_v2*, uint32_t, uint32_t) {}
|
||||
void toolCapability(void*, zwp_tablet_tool_v2*, uint32_t) {}
|
||||
void toolDone(void*, zwp_tablet_tool_v2*) {}
|
||||
void toolRemoved(void* data, zwp_tablet_tool_v2*) { tool(data).owner.onToolRemoved(tool(data)); }
|
||||
void toolProximityIn(void* data, zwp_tablet_tool_v2*, uint32_t serial, zwp_tablet_v2*, wl_surface*) {
|
||||
tool(data).owner.onProximityIn(tool(data), serial);
|
||||
}
|
||||
void toolProximityOut(void* data, zwp_tablet_tool_v2*) { tool(data).owner.onProximityOut(tool(data)); }
|
||||
void toolDown(void* data, zwp_tablet_tool_v2*, uint32_t serial) {
|
||||
tool(data).owner.recordSerial(serial);
|
||||
tool(data).contactChange = 1;
|
||||
}
|
||||
void toolUp(void* data, zwp_tablet_tool_v2*) { tool(data).contactChange = -1; }
|
||||
void toolMotion(void* data, zwp_tablet_tool_v2*, wl_fixed_t x, wl_fixed_t y) {
|
||||
tool(data).position = { (halnf) wl_fixed_to_double(x), (halnf) wl_fixed_to_double(y) };
|
||||
tool(data).moved = true;
|
||||
}
|
||||
void toolPressure(void* data, zwp_tablet_tool_v2*, uint32_t pressure) {
|
||||
tool(data).sample.pressure = (halnf) pressure / 65535.0f;
|
||||
}
|
||||
void toolDistance(void* data, zwp_tablet_tool_v2*, uint32_t distance) {
|
||||
tool(data).sample.distance = (halnf) distance / 65535.0f;
|
||||
}
|
||||
void toolTilt(void* data, zwp_tablet_tool_v2*, wl_fixed_t x, wl_fixed_t y) {
|
||||
tool(data).sample.tilt = { (halnf) wl_fixed_to_double(x), (halnf) wl_fixed_to_double(y) };
|
||||
}
|
||||
void toolRotation(void* data, zwp_tablet_tool_v2*, wl_fixed_t degrees) {
|
||||
tool(data).sample.rotation = (halnf) wl_fixed_to_double(degrees);
|
||||
}
|
||||
void toolSlider(void*, zwp_tablet_tool_v2*, int32_t) {}
|
||||
void toolWheel(void*, zwp_tablet_tool_v2*, wl_fixed_t, int32_t) {}
|
||||
void toolButton(void* data, zwp_tablet_tool_v2*, uint32_t serial, uint32_t button, uint32_t state) {
|
||||
tool(data).owner.recordSerial(serial);
|
||||
const bool pressed = state == ZWP_TABLET_TOOL_V2_BUTTON_STATE_PRESSED;
|
||||
if (button == BTN_STYLUS) {
|
||||
tool(data).linuxButtons.push_back({ BTN_RIGHT, pressed });
|
||||
} else if (button == BTN_STYLUS2) {
|
||||
tool(data).linuxButtons.push_back({ BTN_MIDDLE, pressed });
|
||||
}
|
||||
}
|
||||
void toolFrame(void* data, zwp_tablet_tool_v2*, uint32_t time) { tool(data).owner.onFrame(tool(data), time); }
|
||||
|
||||
void padGroup(void*, zwp_tablet_pad_v2*, zwp_tablet_pad_group_v2*) {}
|
||||
void padPath(void*, zwp_tablet_pad_v2*, const char*) {}
|
||||
void padButtons(void*, zwp_tablet_pad_v2*, uint32_t) {}
|
||||
void padDone(void*, zwp_tablet_pad_v2*) {}
|
||||
void padButton(void*, zwp_tablet_pad_v2*, uint32_t, uint32_t, uint32_t) {}
|
||||
void padEnter(void*, zwp_tablet_pad_v2*, uint32_t, zwp_tablet_v2*, wl_surface*) {}
|
||||
void padLeave(void*, zwp_tablet_pad_v2*, uint32_t, wl_surface*) {}
|
||||
void padRemoved(void*, zwp_tablet_pad_v2*) {}
|
||||
|
||||
const zwp_tablet_seat_v2_listener kTabletSeatListener = {
|
||||
tabletAdded,
|
||||
toolAdded,
|
||||
padAdded,
|
||||
};
|
||||
|
||||
const zwp_tablet_v2_listener kTabletListener = {
|
||||
tabletName, tabletId, tabletPath, tabletDone, tabletRemoved, tabletBustype,
|
||||
};
|
||||
|
||||
const zwp_tablet_tool_v2_listener kToolListener = {
|
||||
toolType, toolSerial, toolWacomId, toolCapability, toolDone, toolRemoved, toolProximityIn,
|
||||
toolProximityOut, toolDown, toolUp, toolMotion, toolPressure, toolDistance, toolTilt,
|
||||
toolRotation, toolSlider, toolWheel, toolButton, toolFrame,
|
||||
};
|
||||
|
||||
const zwp_tablet_pad_v2_listener kPadListener = {
|
||||
padGroup, padPath, padButtons, padDone, padButton, padEnter, padLeave, padRemoved,
|
||||
};
|
||||
}
|
||||
|
||||
WaylandTablet::WaylandTablet(WaylandSeatContext& seat, zwp_tablet_manager_v2* manager, wl_seat* waylandSeat) :
|
||||
mSeat(seat) {
|
||||
mTabletSeat = zwp_tablet_manager_v2_get_tablet_seat(manager, waylandSeat);
|
||||
zwp_tablet_seat_v2_add_listener(mTabletSeat, &kTabletSeatListener, this);
|
||||
}
|
||||
|
||||
WaylandTablet::~WaylandTablet() {
|
||||
for (const auto& tool : mTools) {
|
||||
if (tool->shapeDevice) {
|
||||
wp_cursor_shape_device_v1_destroy(tool->shapeDevice);
|
||||
}
|
||||
zwp_tablet_tool_v2_destroy(tool->handle);
|
||||
}
|
||||
for (zwp_tablet_pad_v2* pad : mPads) {
|
||||
zwp_tablet_pad_v2_destroy(pad);
|
||||
}
|
||||
for (zwp_tablet_v2* tablet : mTablets) {
|
||||
zwp_tablet_v2_destroy(tablet);
|
||||
}
|
||||
zwp_tablet_seat_v2_destroy(mTabletSeat);
|
||||
}
|
||||
|
||||
void WaylandTablet::onTabletAdded(zwp_tablet_v2* tablet) {
|
||||
mTablets.push_back(tablet);
|
||||
zwp_tablet_v2_add_listener(tablet, &kTabletListener, this);
|
||||
}
|
||||
|
||||
void WaylandTablet::onPadAdded(zwp_tablet_pad_v2* pad) {
|
||||
mPads.push_back(pad);
|
||||
zwp_tablet_pad_v2_add_listener(pad, &kPadListener, this);
|
||||
}
|
||||
|
||||
void WaylandTablet::onToolAdded(zwp_tablet_tool_v2* handle) {
|
||||
mTools.push_back(std::make_unique<Tool>(Tool{ *this, handle }));
|
||||
Tool& tool = *mTools.back();
|
||||
zwp_tablet_tool_v2_add_listener(handle, &kToolListener, &tool);
|
||||
|
||||
wp_cursor_shape_manager_v1* manager = mSeat.connection().cursorShapeManager();
|
||||
if (manager) {
|
||||
tool.shapeDevice = wp_cursor_shape_manager_v1_get_tablet_tool_v2(manager, handle);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandTablet::onToolRemoved(Tool& tool) {
|
||||
if (tool.inside) {
|
||||
onProximityOut(tool);
|
||||
}
|
||||
if (tool.shapeDevice) {
|
||||
wp_cursor_shape_device_v1_destroy(tool.shapeDevice);
|
||||
}
|
||||
zwp_tablet_tool_v2_destroy(tool.handle);
|
||||
std::erase_if(mTools, [&tool](const std::unique_ptr<Tool>& entry) { return entry.get() == &tool; });
|
||||
}
|
||||
|
||||
void WaylandTablet::onToolType(Tool& tool, uint32_t type) {
|
||||
tool.device = PointerDevice::Stylus;
|
||||
tool.tip = StylusTip::Pen;
|
||||
switch (type) {
|
||||
case ZWP_TABLET_TOOL_V2_TYPE_ERASER: tool.tip = StylusTip::Eraser; break;
|
||||
case ZWP_TABLET_TOOL_V2_TYPE_BRUSH: tool.tip = StylusTip::Brush; break;
|
||||
case ZWP_TABLET_TOOL_V2_TYPE_PENCIL: tool.tip = StylusTip::Pencil; break;
|
||||
case ZWP_TABLET_TOOL_V2_TYPE_AIRBRUSH: tool.tip = StylusTip::Airbrush; break;
|
||||
case ZWP_TABLET_TOOL_V2_TYPE_FINGER: tool.device = PointerDevice::Finger; break;
|
||||
case ZWP_TABLET_TOOL_V2_TYPE_MOUSE:
|
||||
case ZWP_TABLET_TOOL_V2_TYPE_LENS: tool.device = PointerDevice::Mouse; break;
|
||||
default: break;
|
||||
}
|
||||
tool.sample.device = tool.device;
|
||||
tool.sample.tip = tool.tip;
|
||||
}
|
||||
|
||||
void WaylandTablet::onProximityIn(Tool& tool, uint32_t serial) {
|
||||
tool.inside = true;
|
||||
tool.sample = PointerSample{ tool.device, tool.tip, 0 };
|
||||
if (tool.shapeDevice) {
|
||||
wp_cursor_shape_device_v1_set_shape(tool.shapeDevice, serial, WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandTablet::onProximityOut(Tool& tool) {
|
||||
tool.inside = false;
|
||||
tool.contactChange = -1;
|
||||
onFrame(tool, tool.sample.timeMs);
|
||||
mSeat.surfacePointer().onPointerSurfaceLeft();
|
||||
}
|
||||
|
||||
void WaylandTablet::onFrame(Tool& tool, uint32_t time) {
|
||||
tool.sample.timeMs = time;
|
||||
mSeat.input().post(PointerSampled{ tool.sample });
|
||||
|
||||
if (tool.moved && tool.inside) {
|
||||
mSeat.surfacePointer().onPointerSurfaceMove(tool.position);
|
||||
}
|
||||
tool.moved = false;
|
||||
|
||||
if (tool.contactChange != 0) {
|
||||
mSeat.surfacePointer().onPointerSurfaceButton(BTN_LEFT, tool.contactChange > 0, mSeat.lastInputSerial());
|
||||
tool.contactChange = 0;
|
||||
}
|
||||
|
||||
for (const auto& [linuxButton, pressed] : tool.linuxButtons) {
|
||||
mSeat.surfacePointer().onPointerSurfaceButton(linuxButton, pressed, mSeat.lastInputSerial());
|
||||
}
|
||||
tool.linuxButtons.clear();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
#pragma once
|
||||
|
||||
#include "WaylandSeatContext.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
struct wl_seat;
|
||||
struct wp_cursor_shape_device_v1;
|
||||
struct zwp_tablet_manager_v2;
|
||||
struct zwp_tablet_seat_v2;
|
||||
struct zwp_tablet_v2;
|
||||
struct zwp_tablet_tool_v2;
|
||||
struct zwp_tablet_pad_v2;
|
||||
|
||||
namespace tp {
|
||||
|
||||
// Tablet tools (pens, erasers, ...) reported as pointer samples, motion and buttons.
|
||||
class WaylandTablet {
|
||||
public:
|
||||
WaylandTablet(WaylandSeatContext& seat, zwp_tablet_manager_v2* manager, wl_seat* waylandSeat);
|
||||
~WaylandTablet();
|
||||
|
||||
WaylandTablet(const WaylandTablet&) = delete;
|
||||
WaylandTablet& operator=(const WaylandTablet&) = delete;
|
||||
|
||||
struct Tool {
|
||||
WaylandTablet& owner;
|
||||
zwp_tablet_tool_v2* handle = nullptr;
|
||||
wp_cursor_shape_device_v1* shapeDevice = nullptr;
|
||||
PointerSample sample;
|
||||
PointerDevice device = PointerDevice::Stylus;
|
||||
StylusTip tip = StylusTip::Pen;
|
||||
Vec2F position = { 0, 0 };
|
||||
bool inside = false;
|
||||
bool moved = false;
|
||||
int contactChange = 0;
|
||||
std::vector<std::pair<uint32_t, bool>> linuxButtons;
|
||||
};
|
||||
|
||||
void onTabletAdded(zwp_tablet_v2* tablet);
|
||||
void onToolAdded(zwp_tablet_tool_v2* tool);
|
||||
void onPadAdded(zwp_tablet_pad_v2* pad);
|
||||
|
||||
void onToolType(Tool& tool, uint32_t type);
|
||||
void onProximityIn(Tool& tool, uint32_t serial);
|
||||
void onProximityOut(Tool& tool);
|
||||
void onFrame(Tool& tool, uint32_t time);
|
||||
void onToolRemoved(Tool& tool);
|
||||
void recordSerial(uint32_t serial) { mSeat.recordSerial(serial); }
|
||||
|
||||
private:
|
||||
WaylandSeatContext& mSeat;
|
||||
zwp_tablet_seat_v2* mTabletSeat = nullptr;
|
||||
std::vector<zwp_tablet_v2*> mTablets;
|
||||
std::vector<zwp_tablet_pad_v2*> mPads;
|
||||
std::vector<std::unique_ptr<Tool>> mTools;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
#include "WaylandTextInput.hpp"
|
||||
|
||||
#include "text-input-unstable-v3-client-protocol.h"
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
WaylandTextInput& textInput(void* data) { return *static_cast<WaylandTextInput*>(data); }
|
||||
|
||||
void textEnter(void* data, zwp_text_input_v3*, wl_surface*) { textInput(data).onEnter(); }
|
||||
void textLeave(void* data, zwp_text_input_v3*, wl_surface*) { textInput(data).onLeave(); }
|
||||
void textPreedit(void*, zwp_text_input_v3*, const char*, int32_t, int32_t) {}
|
||||
void textCommit(void* data, zwp_text_input_v3*, const char* text) { textInput(data).onCommitString(text); }
|
||||
void textDeleteSurrounding(void*, zwp_text_input_v3*, uint32_t, uint32_t) {}
|
||||
void textDone(void* data, zwp_text_input_v3*, uint32_t) { textInput(data).onDone(); }
|
||||
|
||||
const zwp_text_input_v3_listener kTextInputListener = {
|
||||
textEnter, textLeave, textPreedit, textCommit, textDeleteSurrounding, textDone,
|
||||
};
|
||||
}
|
||||
|
||||
WaylandTextInput::WaylandTextInput(
|
||||
WaylandSeatContext& seat, zwp_text_input_manager_v3* manager, wl_seat* waylandSeat
|
||||
) :
|
||||
mSeat(seat) {
|
||||
mTextInput = zwp_text_input_manager_v3_get_text_input(manager, waylandSeat);
|
||||
zwp_text_input_v3_add_listener(mTextInput, &kTextInputListener, this);
|
||||
}
|
||||
|
||||
WaylandTextInput::~WaylandTextInput() {
|
||||
if (mTextInput) {
|
||||
zwp_text_input_v3_destroy(mTextInput);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandTextInput::setEnabled(bool enabled) {
|
||||
if (mEnabled == enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
mEnabled = enabled;
|
||||
sendState();
|
||||
}
|
||||
|
||||
void WaylandTextInput::setCursorRect(const RectF& surfaceRect) {
|
||||
if (mCursorRect == surfaceRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
mCursorRect = surfaceRect;
|
||||
if (mEnabled && mFocused) {
|
||||
sendState();
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandTextInput::onEnter() {
|
||||
mFocused = true;
|
||||
sendState();
|
||||
}
|
||||
|
||||
void WaylandTextInput::onLeave() {
|
||||
mFocused = false;
|
||||
zwp_text_input_v3_disable(mTextInput);
|
||||
zwp_text_input_v3_commit(mTextInput);
|
||||
}
|
||||
|
||||
void WaylandTextInput::onCommitString(const char* text) { mPendingCommit = text ? text : ""; }
|
||||
|
||||
void WaylandTextInput::onDone() {
|
||||
if (!mPendingCommit.empty()) {
|
||||
mSeat.input().post(TextEntered{ mPendingCommit });
|
||||
}
|
||||
mPendingCommit.clear();
|
||||
}
|
||||
|
||||
void WaylandTextInput::sendState() {
|
||||
if (!mFocused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mEnabled) {
|
||||
zwp_text_input_v3_enable(mTextInput);
|
||||
zwp_text_input_v3_set_content_type(
|
||||
mTextInput, ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL
|
||||
);
|
||||
zwp_text_input_v3_set_cursor_rectangle(
|
||||
mTextInput, (int32_t) mCursorRect.x, (int32_t) mCursorRect.y, (int32_t) mCursorRect.z, (int32_t) mCursorRect.w
|
||||
);
|
||||
} else {
|
||||
zwp_text_input_v3_disable(mTextInput);
|
||||
}
|
||||
|
||||
zwp_text_input_v3_commit(mTextInput);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
|
||||
#include "Rect.hpp"
|
||||
#include "WaylandSeatContext.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
struct wl_seat;
|
||||
struct zwp_text_input_manager_v3;
|
||||
struct zwp_text_input_v3;
|
||||
|
||||
namespace tp {
|
||||
|
||||
// Input-method text through zwp_text_input_v3.
|
||||
class WaylandTextInput {
|
||||
public:
|
||||
WaylandTextInput(WaylandSeatContext& seat, zwp_text_input_manager_v3* manager, wl_seat* waylandSeat);
|
||||
~WaylandTextInput();
|
||||
|
||||
WaylandTextInput(const WaylandTextInput&) = delete;
|
||||
WaylandTextInput& operator=(const WaylandTextInput&) = delete;
|
||||
|
||||
void setEnabled(bool enabled);
|
||||
void setCursorRect(const RectF& surfaceRect);
|
||||
|
||||
void onEnter();
|
||||
void onLeave();
|
||||
void onCommitString(const char* text);
|
||||
void onDone();
|
||||
|
||||
private:
|
||||
void sendState();
|
||||
|
||||
WaylandSeatContext& mSeat;
|
||||
zwp_text_input_v3* mTextInput = nullptr;
|
||||
bool mFocused = false;
|
||||
bool mEnabled = false;
|
||||
RectF mCursorRect = { 0, 0, 1, 1 };
|
||||
std::string mPendingCommit;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,665 @@
|
|||
#include "WaylandWindow.hpp"
|
||||
|
||||
#include <sys/eventfd.h>
|
||||
|
||||
#include "LinuxInputCodes.hpp"
|
||||
#include "WaylandConnection.hpp"
|
||||
#include "WaylandSeat.hpp"
|
||||
|
||||
#include "Timing.hpp"
|
||||
|
||||
#include "fractional-scale-v1-client-protocol.h"
|
||||
#include "viewporter-client-protocol.h"
|
||||
#include "xdg-activation-v1-client-protocol.h"
|
||||
#include "xdg-decoration-unstable-v1-client-protocol.h"
|
||||
#include "xdg-shell-client-protocol.h"
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <vulkan/vulkan_wayland.h>
|
||||
#include <wayland-client.h>
|
||||
|
||||
#include <linux/input-event-codes.h>
|
||||
#include <poll.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
constexpr halnf kFractionalScaleDenominator = 120;
|
||||
constexpr halnf kMinimumContentWidth = 320;
|
||||
constexpr halnf kMinimumContentHeight = 200;
|
||||
constexpr int kConfigureWaitMs = 1000;
|
||||
}
|
||||
|
||||
struct WaylandWindow::Objects {
|
||||
wl_surface* surface = nullptr;
|
||||
xdg_surface* shellSurface = nullptr;
|
||||
xdg_toplevel* toplevel = nullptr;
|
||||
wp_viewport* viewport = nullptr;
|
||||
wp_fractional_scale_v1* fractionalScale = nullptr;
|
||||
zxdg_toplevel_decoration_v1* decoration = nullptr;
|
||||
WaylandConfigure pending;
|
||||
wl_callback* frameCallback = nullptr;
|
||||
};
|
||||
|
||||
struct WaylandListeners {
|
||||
static WaylandWindow& window(void* data) { return *static_cast<WaylandWindow*>(data); }
|
||||
|
||||
static void surfaceConfigure(void* data, xdg_surface*, uint32_t serial) {
|
||||
WaylandWindow& self = window(data);
|
||||
WaylandConfigure configure = self.mObjects->pending;
|
||||
configure.serial = serial;
|
||||
self.mPendingConfigure = configure;
|
||||
}
|
||||
|
||||
static void toplevelConfigure(void* data, xdg_toplevel*, int32_t width, int32_t height, wl_array* states) {
|
||||
WaylandConfigure configure;
|
||||
configure.width = width;
|
||||
configure.height = height;
|
||||
|
||||
const auto* entries = static_cast<const uint32_t*>(states->data);
|
||||
const size_t count = states->size / sizeof(uint32_t);
|
||||
for (size_t index = 0; index < count; index++) {
|
||||
switch (entries[index]) {
|
||||
case XDG_TOPLEVEL_STATE_MAXIMIZED: configure.maximized = true; break;
|
||||
case XDG_TOPLEVEL_STATE_FULLSCREEN: configure.fullscreen = true; break;
|
||||
case XDG_TOPLEVEL_STATE_ACTIVATED: configure.activated = true; break;
|
||||
case XDG_TOPLEVEL_STATE_TILED_LEFT:
|
||||
case XDG_TOPLEVEL_STATE_TILED_RIGHT:
|
||||
case XDG_TOPLEVEL_STATE_TILED_TOP:
|
||||
case XDG_TOPLEVEL_STATE_TILED_BOTTOM: configure.tiled = true; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
window(data).mObjects->pending = configure;
|
||||
}
|
||||
|
||||
static void toplevelClose(void* data, xdg_toplevel*) { window(data).mCloseRequested = true; }
|
||||
static void toplevelBounds(void*, xdg_toplevel*, int32_t, int32_t) {}
|
||||
static void toplevelCapabilities(void*, xdg_toplevel*, wl_array*) {}
|
||||
|
||||
static void preferredScale(void* data, wp_fractional_scale_v1*, uint32_t scale) {
|
||||
window(data).applyScale((halnf) scale / kFractionalScaleDenominator);
|
||||
}
|
||||
|
||||
static void decorationConfigure(void*, zxdg_toplevel_decoration_v1*, uint32_t) {}
|
||||
|
||||
static void frameDone(void* data, wl_callback* callback, uint32_t) {
|
||||
WaylandWindow& self = window(data);
|
||||
wl_callback_destroy(callback);
|
||||
if (self.mObjects->frameCallback == callback) {
|
||||
self.mObjects->frameCallback = nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
namespace {
|
||||
const xdg_surface_listener kShellSurfaceListener = { WaylandListeners::surfaceConfigure };
|
||||
|
||||
const xdg_toplevel_listener kToplevelListener = {
|
||||
WaylandListeners::toplevelConfigure,
|
||||
WaylandListeners::toplevelClose,
|
||||
WaylandListeners::toplevelBounds,
|
||||
WaylandListeners::toplevelCapabilities,
|
||||
};
|
||||
|
||||
const wp_fractional_scale_v1_listener kFractionalScaleListener = { WaylandListeners::preferredScale };
|
||||
|
||||
const zxdg_toplevel_decoration_v1_listener kDecorationListener = { WaylandListeners::decorationConfigure };
|
||||
const wl_callback_listener kFrameListener = { WaylandListeners::frameDone };
|
||||
|
||||
uint32_t resizeEdgeCode(WindowEdge edge) {
|
||||
switch (edge) {
|
||||
case WindowEdge::Top: return XDG_TOPLEVEL_RESIZE_EDGE_TOP;
|
||||
case WindowEdge::Bottom: return XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM;
|
||||
case WindowEdge::Left: return XDG_TOPLEVEL_RESIZE_EDGE_LEFT;
|
||||
case WindowEdge::Right: return XDG_TOPLEVEL_RESIZE_EDGE_RIGHT;
|
||||
case WindowEdge::TopLeft: return XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT;
|
||||
case WindowEdge::TopRight: return XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT;
|
||||
case WindowEdge::BottomLeft: return XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT;
|
||||
case WindowEdge::BottomRight: return XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT;
|
||||
case WindowEdge::None: return XDG_TOPLEVEL_RESIZE_EDGE_NONE;
|
||||
}
|
||||
return XDG_TOPLEVEL_RESIZE_EDGE_NONE;
|
||||
}
|
||||
|
||||
uint32_t scaledLength(halnf length, halnf scale) { return (uint32_t) std::lround(length * scale); }
|
||||
}
|
||||
|
||||
WaylandWindow::WaylandWindow(PlatformWindowHost& host, Vec2F contentSize, const WindowIdentity& identity) :
|
||||
mHost(host),
|
||||
mConnection(std::make_unique<WaylandConnection>()),
|
||||
mObjects(std::make_unique<Objects>()),
|
||||
mWakeDescriptor(eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK)) {
|
||||
mFrame.setContentSize(contentSize);
|
||||
mRestoredContentSize = contentSize;
|
||||
|
||||
WaylandConnection& connection = *mConnection;
|
||||
Objects& objects = *mObjects;
|
||||
|
||||
objects.surface = wl_compositor_create_surface(connection.compositor());
|
||||
objects.shellSurface = xdg_wm_base_get_xdg_surface(connection.windowManager(), objects.surface);
|
||||
xdg_surface_add_listener(objects.shellSurface, &kShellSurfaceListener, this);
|
||||
|
||||
objects.toplevel = xdg_surface_get_toplevel(objects.shellSurface);
|
||||
xdg_toplevel_add_listener(objects.toplevel, &kToplevelListener, this);
|
||||
xdg_toplevel_set_title(objects.toplevel, identity.title.c_str());
|
||||
xdg_toplevel_set_app_id(objects.toplevel, identity.appId.c_str());
|
||||
xdg_toplevel_set_min_size(objects.toplevel, (int32_t) kMinimumContentWidth, (int32_t) kMinimumContentHeight);
|
||||
|
||||
if (connection.decorationManager()) {
|
||||
objects.decoration =
|
||||
zxdg_decoration_manager_v1_get_toplevel_decoration(connection.decorationManager(), objects.toplevel);
|
||||
zxdg_toplevel_decoration_v1_add_listener(objects.decoration, &kDecorationListener, this);
|
||||
zxdg_toplevel_decoration_v1_set_mode(objects.decoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE);
|
||||
}
|
||||
|
||||
if (connection.viewporter()) {
|
||||
objects.viewport = wp_viewporter_get_viewport(connection.viewporter(), objects.surface);
|
||||
}
|
||||
if (connection.fractionalScaleManager() && objects.viewport) {
|
||||
objects.fractionalScale =
|
||||
wp_fractional_scale_manager_v1_get_fractional_scale(connection.fractionalScaleManager(), objects.surface);
|
||||
wp_fractional_scale_v1_add_listener(objects.fractionalScale, &kFractionalScaleListener, this);
|
||||
}
|
||||
|
||||
mSeat = std::make_unique<WaylandSeat>(connection, mHost, *this);
|
||||
|
||||
updateFrameGeometry();
|
||||
wl_surface_commit(objects.surface);
|
||||
connection.flush();
|
||||
}
|
||||
|
||||
WaylandWindow::~WaylandWindow() {
|
||||
if (mWakeDescriptor >= 0) {
|
||||
close(mWakeDescriptor);
|
||||
}
|
||||
mSeat.reset();
|
||||
|
||||
Objects& objects = *mObjects;
|
||||
if (objects.frameCallback) {
|
||||
wl_callback_destroy(objects.frameCallback);
|
||||
}
|
||||
if (objects.decoration) {
|
||||
zxdg_toplevel_decoration_v1_destroy(objects.decoration);
|
||||
}
|
||||
if (objects.fractionalScale) {
|
||||
wp_fractional_scale_v1_destroy(objects.fractionalScale);
|
||||
}
|
||||
if (objects.viewport) {
|
||||
wp_viewport_destroy(objects.viewport);
|
||||
}
|
||||
if (objects.toplevel) {
|
||||
xdg_toplevel_destroy(objects.toplevel);
|
||||
}
|
||||
if (objects.shellSurface) {
|
||||
xdg_surface_destroy(objects.shellSurface);
|
||||
}
|
||||
if (objects.surface) {
|
||||
wl_surface_destroy(objects.surface);
|
||||
}
|
||||
mConnection->flush();
|
||||
}
|
||||
|
||||
VkSurfaceKHR WaylandWindow::createSurface(VkInstance instance) const {
|
||||
VkWaylandSurfaceCreateInfoKHR createInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR,
|
||||
.display = mConnection->display(),
|
||||
.surface = mObjects->surface,
|
||||
};
|
||||
|
||||
VkSurfaceKHR surface = VK_NULL_HANDLE;
|
||||
if (vkCreateWaylandSurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
return surface;
|
||||
}
|
||||
|
||||
VkExtent2D WaylandWindow::framebufferExtent() const {
|
||||
const Vec2F surface = mFrame.surfaceSize();
|
||||
return { scaledLength(surface.x, mScale), scaledLength(surface.y, mScale) };
|
||||
}
|
||||
|
||||
void WaylandWindow::onTranslucencyResolved(bool translucent) {
|
||||
mFrame.setTranslucent(translucent);
|
||||
mGeometryDirty = true;
|
||||
}
|
||||
|
||||
void WaylandWindow::dispatchEvents(halnf timeoutMs) {
|
||||
const bool waitsForever = timeoutMs < 0;
|
||||
const time_ns deadline = get_time_ns() + (time_ns) (std::max(timeoutMs, 0.0f) * 1.0e6f);
|
||||
|
||||
while (true) {
|
||||
const halnf remainingMs = waitsForever ? -1.0f : (halnf) ((double) (deadline - get_time_ns()) / 1.0e6);
|
||||
if (pollOnce(waitsForever ? -1 : std::max(0, (int) std::ceil(remainingMs)))) {
|
||||
return;
|
||||
}
|
||||
if (!waitsForever && get_time_ns() >= deadline) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandWindow::wake() {
|
||||
if (mWakeDescriptor >= 0) {
|
||||
eventfd_write(mWakeDescriptor, 1);
|
||||
}
|
||||
}
|
||||
|
||||
bool WaylandWindow::pollOnce(int timeoutMs) {
|
||||
wl_display* display = mConnection->display();
|
||||
|
||||
mConnection->flush();
|
||||
if (wl_display_dispatch_pending(display) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
while (wl_display_prepare_read(display) != 0) {
|
||||
if (wl_display_dispatch_pending(display) > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
mConnection->flush();
|
||||
|
||||
std::vector<pollfd> descriptors = {
|
||||
{ wl_display_get_fd(display), POLLIN, 0 },
|
||||
{ mWakeDescriptor, POLLIN, 0 },
|
||||
};
|
||||
mSeat->collectPollDescriptors(descriptors);
|
||||
|
||||
const int ready = poll(descriptors.data(), (nfds_t) descriptors.size(), timeoutMs);
|
||||
if (ready > 0 && (descriptors[0].revents & POLLIN)) {
|
||||
if (wl_display_read_events(display) < 0) {
|
||||
throw std::runtime_error("lost the Wayland connection");
|
||||
}
|
||||
} else {
|
||||
wl_display_cancel_read(display);
|
||||
}
|
||||
|
||||
bool handled = wl_display_dispatch_pending(display) > 0;
|
||||
if (ready > 0 && (descriptors[1].revents & POLLIN)) {
|
||||
eventfd_t ignored = 0;
|
||||
eventfd_read(mWakeDescriptor, &ignored);
|
||||
handled = true;
|
||||
}
|
||||
if (ready > 0) {
|
||||
const std::vector<pollfd> seatDescriptors(descriptors.begin() + 2, descriptors.end());
|
||||
mSeat->onDescriptorsReady(seatDescriptors);
|
||||
handled = handled || std::any_of(seatDescriptors.begin(), seatDescriptors.end(), [](const pollfd& entry) {
|
||||
return entry.revents != 0;
|
||||
});
|
||||
}
|
||||
|
||||
mConnection->flush();
|
||||
return handled;
|
||||
}
|
||||
|
||||
void WaylandWindow::waitUntilConfigured() {
|
||||
while (!mConfigured) {
|
||||
if (!mConnection->dispatch(kConfigureWaitMs)) {
|
||||
break;
|
||||
}
|
||||
applyPendingConfigure();
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandWindow::applyPendingConfigure() {
|
||||
if (!mPendingConfigure) {
|
||||
return;
|
||||
}
|
||||
const WaylandConfigure configure = *mPendingConfigure;
|
||||
mPendingConfigure.reset();
|
||||
|
||||
applyConfigure(configure);
|
||||
xdg_surface_ack_configure(mObjects->shellSurface, configure.serial);
|
||||
mConfigured = true;
|
||||
notifyOwnerResized();
|
||||
}
|
||||
|
||||
void WaylandWindow::applyConfigure(const WaylandConfigure& configure) {
|
||||
mMaximized = configure.maximized || configure.fullscreen;
|
||||
mFullScreen = configure.fullscreen;
|
||||
mActivated = configure.activated;
|
||||
|
||||
const bool tiled = mMaximized || configure.tiled;
|
||||
if (tiled != mFrame.isTiled()) {
|
||||
mFrame.setTiled(tiled);
|
||||
mGeometryDirty = true;
|
||||
}
|
||||
|
||||
Vec2F contentSize = mFrame.contentSize();
|
||||
if (configure.width > 0 && configure.height > 0) {
|
||||
contentSize = { (halnf) configure.width, (halnf) configure.height };
|
||||
} else if (!tiled) {
|
||||
contentSize = mRestoredContentSize;
|
||||
}
|
||||
|
||||
contentSize.x = std::max(contentSize.x, kMinimumContentWidth);
|
||||
contentSize.y = std::max(contentSize.y, kMinimumContentHeight);
|
||||
if (!tiled) {
|
||||
mRestoredContentSize = contentSize;
|
||||
}
|
||||
|
||||
if (contentSize != mFrame.contentSize()) {
|
||||
mFrame.setContentSize(contentSize);
|
||||
mGeometryDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandWindow::applyScale(halnf scale) {
|
||||
if (scale <= 0 || scale == mScale) {
|
||||
return;
|
||||
}
|
||||
|
||||
mScale = scale;
|
||||
mGeometryDirty = true;
|
||||
if (mConfigured) {
|
||||
notifyOwnerResized();
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandWindow::notifyOwnerResized() { mHost.onContentResized(); }
|
||||
|
||||
void WaylandWindow::setAcceptsInput(bool accepts) {
|
||||
mAcceptsInput = accepts;
|
||||
mGeometryDirty = true;
|
||||
updateFrameGeometry();
|
||||
wl_surface_commit(mObjects->surface);
|
||||
mConnection->flush();
|
||||
}
|
||||
|
||||
void WaylandWindow::commitFrameGeometry() {
|
||||
if (mGeometryDirty) {
|
||||
updateFrameGeometry();
|
||||
}
|
||||
requestFrameCallback();
|
||||
}
|
||||
|
||||
void WaylandWindow::requestFrameCallback() {
|
||||
if (mObjects->frameCallback) {
|
||||
return;
|
||||
}
|
||||
mObjects->frameCallback = wl_surface_frame(mObjects->surface);
|
||||
wl_callback_add_listener(mObjects->frameCallback, &kFrameListener, this);
|
||||
}
|
||||
|
||||
bool WaylandWindow::isWaitingForFrame() const { return mObjects->frameCallback != nullptr; }
|
||||
|
||||
void WaylandWindow::updateFrameGeometry() {
|
||||
Objects& objects = *mObjects;
|
||||
const Vec2F surface = mFrame.surfaceSize();
|
||||
const RectF content = mFrame.contentRect();
|
||||
const RectF input = mFrame.inputRect();
|
||||
|
||||
xdg_surface_set_window_geometry(
|
||||
objects.shellSurface, (int32_t) content.x, (int32_t) content.y, (int32_t) content.z, (int32_t) content.w
|
||||
);
|
||||
|
||||
wl_region* inputRegion = wl_compositor_create_region(mConnection->compositor());
|
||||
if (mAcceptsInput) {
|
||||
wl_region_add(inputRegion, (int32_t) input.x, (int32_t) input.y, (int32_t) input.z, (int32_t) input.w);
|
||||
}
|
||||
wl_surface_set_input_region(objects.surface, inputRegion);
|
||||
wl_region_destroy(inputRegion);
|
||||
|
||||
if (!mFrame.hasShadow()) {
|
||||
wl_region* opaqueRegion = wl_compositor_create_region(mConnection->compositor());
|
||||
wl_region_add(opaqueRegion, 0, 0, (int32_t) surface.x, (int32_t) surface.y);
|
||||
wl_surface_set_opaque_region(objects.surface, opaqueRegion);
|
||||
wl_region_destroy(opaqueRegion);
|
||||
} else {
|
||||
wl_surface_set_opaque_region(objects.surface, nullptr);
|
||||
}
|
||||
|
||||
if (objects.viewport) {
|
||||
wp_viewport_set_destination(objects.viewport, (int32_t) surface.x, (int32_t) surface.y);
|
||||
} else {
|
||||
wl_surface_set_buffer_scale(objects.surface, (int32_t) std::lround(mScale));
|
||||
}
|
||||
|
||||
mGeometryDirty = false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct ActivationToken {
|
||||
std::string value;
|
||||
bool done = false;
|
||||
};
|
||||
|
||||
void activationTokenDone(void* data, xdg_activation_token_v1*, const char* token) {
|
||||
auto& result = *static_cast<ActivationToken*>(data);
|
||||
result.value = token ? token : "";
|
||||
result.done = true;
|
||||
}
|
||||
|
||||
const xdg_activation_token_v1_listener kActivationTokenListener = { activationTokenDone };
|
||||
}
|
||||
|
||||
std::string WaylandWindow::activationToken() {
|
||||
xdg_activation_v1* activation = mConnection->activation();
|
||||
if (!activation) {
|
||||
return {};
|
||||
}
|
||||
|
||||
wl_display* display = mConnection->display();
|
||||
wl_event_queue* queue = wl_display_create_queue(display);
|
||||
auto* wrapped = static_cast<xdg_activation_v1*>(wl_proxy_create_wrapper(activation));
|
||||
wl_proxy_set_queue(reinterpret_cast<wl_proxy*>(wrapped), queue);
|
||||
|
||||
ActivationToken result;
|
||||
xdg_activation_token_v1* token = xdg_activation_v1_get_activation_token(wrapped);
|
||||
xdg_activation_token_v1_add_listener(token, &kActivationTokenListener, &result);
|
||||
if (mConnection->seat() && mSeat->lastInputSerial() != 0) {
|
||||
xdg_activation_token_v1_set_serial(token, mSeat->lastInputSerial(), mConnection->seat());
|
||||
}
|
||||
xdg_activation_token_v1_set_surface(token, mObjects->surface);
|
||||
xdg_activation_token_v1_commit(token);
|
||||
while (!result.done && wl_display_roundtrip_queue(display, queue) >= 0) {
|
||||
}
|
||||
|
||||
xdg_activation_token_v1_destroy(token);
|
||||
wl_proxy_wrapper_destroy(wrapped);
|
||||
wl_event_queue_destroy(queue);
|
||||
return result.value;
|
||||
}
|
||||
|
||||
bool WaylandWindow::openUrl(const std::string& url) {
|
||||
if (url.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> environment;
|
||||
for (char** entry = environ; *entry; entry++) {
|
||||
const std::string variable = *entry;
|
||||
if (variable.starts_with("XDG_ACTIVATION_TOKEN=") || variable.starts_with("DESKTOP_STARTUP_ID=")) {
|
||||
continue;
|
||||
}
|
||||
environment.push_back(variable);
|
||||
}
|
||||
const std::string token = activationToken();
|
||||
if (!token.empty()) {
|
||||
environment.push_back("XDG_ACTIVATION_TOKEN=" + token);
|
||||
environment.push_back("DESKTOP_STARTUP_ID=" + token);
|
||||
}
|
||||
|
||||
std::vector<char*> environmentPointers;
|
||||
for (std::string& variable : environment) {
|
||||
environmentPointers.push_back(variable.data());
|
||||
}
|
||||
environmentPointers.push_back(nullptr);
|
||||
|
||||
std::string program = "xdg-open";
|
||||
std::string argument = url;
|
||||
char* arguments[] = { program.data(), argument.data(), nullptr };
|
||||
|
||||
pid_t child = 0;
|
||||
if (posix_spawnp(&child, program.c_str(), nullptr, nullptr, arguments, environmentPointers.data()) != 0) {
|
||||
return false;
|
||||
}
|
||||
std::thread([child]() { waitpid(child, nullptr, 0); }).detach();
|
||||
return true;
|
||||
}
|
||||
|
||||
Vec2F WaylandWindow::toContent(Vec2F surfacePoint) const {
|
||||
const RectF content = mFrame.contentRect();
|
||||
return { surfacePoint.x - content.x, surfacePoint.y - content.y };
|
||||
}
|
||||
|
||||
void WaylandWindow::onPointerSurfaceMove(Vec2F surfacePoint) {
|
||||
const bool contentHoldsPointer = mHeldButtons > 0;
|
||||
const WindowEdge edge = contentHoldsPointer ? WindowEdge::None : mFrame.edgeAt(surfacePoint);
|
||||
mHoveredEdge = edge;
|
||||
|
||||
if (edge != WindowEdge::None) {
|
||||
mSeat->setCursor(cursorForEdge(edge));
|
||||
if (mPointerOnContent) {
|
||||
mPointerOnContent = false;
|
||||
input().post(PointerLeft{});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
mSeat->setCursor(mClientCursor);
|
||||
mPointerOnContent = true;
|
||||
input().post(PointerMoved{ toContent(surfacePoint) });
|
||||
}
|
||||
|
||||
// Touchpad pinches arrive at the pointer; the gesture is placed where the pointer is, in content coordinates.
|
||||
void WaylandWindow::onPointerSurfacePinch(const TouchGesture& gesture) {
|
||||
TouchGesture content = gesture;
|
||||
content.center = toContent(gesture.center);
|
||||
input().post(GestureChanged{ content });
|
||||
}
|
||||
|
||||
void WaylandWindow::onPointerSurfaceLeft() {
|
||||
mHeldButtons = 0;
|
||||
mHoveredEdge = WindowEdge::None;
|
||||
mPointerOnContent = false;
|
||||
input().post(PointerLeft{});
|
||||
}
|
||||
|
||||
void WaylandWindow::onPointerSurfaceButton(uint32_t linuxButton, bool pressed, uint32_t serial) {
|
||||
if (pressed && mHoveredEdge != WindowEdge::None) {
|
||||
if (linuxButton == BTN_LEFT) {
|
||||
xdg_toplevel_resize(mObjects->toplevel, mConnection->seat(), serial, resizeEdgeCode(mHoveredEdge));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const std::optional<PointerButton> button = pointerButtonFromLinux(linuxButton);
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pressed) {
|
||||
mHeldButtons++;
|
||||
} else if (mHeldButtons > 0) {
|
||||
mHeldButtons--;
|
||||
}
|
||||
input().post(PointerButtonChanged{ *button, pressed });
|
||||
}
|
||||
|
||||
void WaylandWindow::minimize() { xdg_toplevel_set_minimized(mObjects->toplevel); }
|
||||
|
||||
void WaylandWindow::toggleMaximize() {
|
||||
if (mMaximized) {
|
||||
xdg_toplevel_unset_maximized(mObjects->toplevel);
|
||||
} else {
|
||||
xdg_toplevel_set_maximized(mObjects->toplevel);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandWindow::setFullScreen(bool fullScreen) {
|
||||
if (fullScreen) {
|
||||
xdg_toplevel_set_fullscreen(mObjects->toplevel, nullptr);
|
||||
} else {
|
||||
xdg_toplevel_unset_fullscreen(mObjects->toplevel);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandWindow::beginMove() {
|
||||
xdg_toplevel_move(mObjects->toplevel, mConnection->seat(), mSeat->lastInputSerial());
|
||||
}
|
||||
|
||||
void WaylandWindow::beginResize(WindowEdge edge) {
|
||||
if (edge == WindowEdge::None) {
|
||||
return;
|
||||
}
|
||||
xdg_toplevel_resize(mObjects->toplevel, mConnection->seat(), mSeat->lastInputSerial(), resizeEdgeCode(edge));
|
||||
}
|
||||
|
||||
void WaylandWindow::showWindowMenu(Vec2F contentPosition) {
|
||||
const RectF content = mFrame.contentRect();
|
||||
xdg_toplevel_show_window_menu(
|
||||
mObjects->toplevel,
|
||||
mConnection->seat(),
|
||||
mSeat->lastInputSerial(),
|
||||
(int32_t) (contentPosition.x + content.x),
|
||||
(int32_t) (contentPosition.y + content.y)
|
||||
);
|
||||
}
|
||||
|
||||
void WaylandWindow::setCursor(CursorShape shape) {
|
||||
mClientCursor = shape;
|
||||
if (mHoveredEdge == WindowEdge::None) {
|
||||
mSeat->setCursor(shape);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandWindow::setClipboardText(const std::string& text) { mSeat->setClipboardText(text); }
|
||||
|
||||
std::string WaylandWindow::clipboardText() { return mSeat->clipboardText(); }
|
||||
|
||||
ClipboardImage WaylandWindow::clipboardImage() { return mSeat->clipboardImage(); }
|
||||
|
||||
void WaylandWindow::setTextInputEnabled(bool enabled) { mSeat->setTextInputEnabled(enabled); }
|
||||
|
||||
void WaylandWindow::setTextInputRect(const RectF& contentRect) {
|
||||
const RectF content = mFrame.contentRect();
|
||||
mSeat->setTextInputRect({ contentRect.x + content.x, contentRect.y + content.y, contentRect.z, contentRect.w });
|
||||
}
|
||||
|
||||
PlatformInfo WaylandWindow::platformInfo() const {
|
||||
PlatformInfo info;
|
||||
info.system = OperatingSystem::Linux;
|
||||
info.formFactor = FormFactor::Desktop;
|
||||
info.contentScale = mScale;
|
||||
info.screenSize = mFrame.contentSize();
|
||||
info.hasMouse = mSeat->hasPointer();
|
||||
info.hasTouch = mSeat->hasTouch();
|
||||
info.hasPen = mSeat->hasTablet();
|
||||
info.hasPhysicalKeyboard = mSeat->hasKeyboard();
|
||||
info.hasHoverPointer = info.hasMouse || info.hasPen;
|
||||
return info;
|
||||
}
|
||||
|
||||
std::vector<const char*> platformSurfaceExtensions() {
|
||||
return { VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME };
|
||||
}
|
||||
|
||||
// Resources are installed next to the executable, so a moved or installed binary still finds them.
|
||||
std::filesystem::path platformResourceDirectory() {
|
||||
std::error_code error;
|
||||
const std::filesystem::path executable = std::filesystem::read_symlink("/proc/self/exe", error);
|
||||
if (error) {
|
||||
return "rsc";
|
||||
}
|
||||
return executable.parent_path() / "rsc";
|
||||
}
|
||||
|
||||
std::unique_ptr<PlatformWindow> createPlatformWindow(
|
||||
PlatformWindowHost& host, Vec2F contentSize, const WindowIdentity& identity, const PlatformContext&
|
||||
) {
|
||||
return std::make_unique<WaylandWindow>(host, contentSize, identity);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
#pragma once
|
||||
|
||||
#include "DesktopWindowControls.hpp"
|
||||
#include "PlatformWindow.hpp"
|
||||
#include "WaylandSeatContext.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
struct wl_display;
|
||||
|
||||
namespace tp {
|
||||
|
||||
class WaylandConnection;
|
||||
class WaylandSeat;
|
||||
|
||||
// One xdg_toplevel's pending configure; applied and acknowledged by the frame loop.
|
||||
struct WaylandConfigure {
|
||||
int32_t width = 0;
|
||||
int32_t height = 0;
|
||||
bool maximized = false;
|
||||
bool fullscreen = false;
|
||||
bool activated = false;
|
||||
bool tiled = false;
|
||||
uint32_t serial = 0;
|
||||
};
|
||||
|
||||
class WaylandWindow final : public PlatformWindow, public DesktopWindowControls, public WaylandSurfacePointer {
|
||||
public:
|
||||
WaylandWindow(PlatformWindowHost& host, Vec2F contentSize, const WindowIdentity& identity);
|
||||
~WaylandWindow() override;
|
||||
|
||||
WaylandWindow(const WaylandWindow&) = delete;
|
||||
WaylandWindow& operator=(const WaylandWindow&) = delete;
|
||||
|
||||
[[nodiscard]] VkSurfaceKHR createSurface(VkInstance instance) const override;
|
||||
[[nodiscard]] VkExtent2D framebufferExtent() const override;
|
||||
[[nodiscard]] bool wantsTranslucency() const override { return true; }
|
||||
|
||||
void setAcceptsInput(bool accepts) override;
|
||||
void onTranslucencyResolved(bool translucent) override;
|
||||
|
||||
void dispatchEvents(halnf timeoutMs) override;
|
||||
void wake() override;
|
||||
void waitUntilConfigured() override;
|
||||
[[nodiscard]] bool hasPendingConfigure() const override { return mPendingConfigure.has_value(); }
|
||||
void applyPendingConfigure() override;
|
||||
void commitFrameGeometry() override;
|
||||
[[nodiscard]] bool isWaitingForFrame() const override;
|
||||
|
||||
[[nodiscard]] bool shouldClose() const override { return mCloseRequested; }
|
||||
void requestClose() override { mCloseRequested = true; }
|
||||
|
||||
[[nodiscard]] const WindowFrame& frame() const override { return mFrame; }
|
||||
[[nodiscard]] halnf scale() const override { return mScale; }
|
||||
[[nodiscard]] bool isMaximized() const override { return mMaximized; }
|
||||
[[nodiscard]] bool isFocused() const override { return mActivated; }
|
||||
|
||||
void minimize() override;
|
||||
[[nodiscard]] DesktopWindowControls* desktopControls() override { return this; }
|
||||
void toggleMaximize() override;
|
||||
void setFullScreen(bool fullScreen) override;
|
||||
[[nodiscard]] bool isFullScreen() const override { return mFullScreen; }
|
||||
void beginMove() override;
|
||||
void beginResize(WindowEdge edge) override;
|
||||
void showWindowMenu(Vec2F contentPosition) override;
|
||||
|
||||
void setCursor(CursorShape shape) override;
|
||||
void setClipboardText(const std::string& text) override;
|
||||
[[nodiscard]] std::string clipboardText() override;
|
||||
[[nodiscard]] ClipboardImage clipboardImage() override;
|
||||
bool openUrl(const std::string& url) override;
|
||||
void setTextInputEnabled(bool enabled) override;
|
||||
void setTextInputRect(const RectF& contentRect) override;
|
||||
|
||||
[[nodiscard]] PlatformInfo platformInfo() const override;
|
||||
|
||||
private:
|
||||
friend struct WaylandListeners;
|
||||
|
||||
void applyConfigure(const WaylandConfigure& configure);
|
||||
void applyScale(halnf scale);
|
||||
void notifyOwnerResized();
|
||||
[[nodiscard]] bool pollOnce(int timeoutMs);
|
||||
void updateFrameGeometry();
|
||||
void requestFrameCallback();
|
||||
|
||||
void onPointerSurfaceMove(Vec2F surfacePoint) override;
|
||||
void onPointerSurfaceButton(uint32_t linuxButton, bool pressed, uint32_t serial) override;
|
||||
void onPointerSurfaceLeft() override;
|
||||
void onPointerSurfacePinch(const TouchGesture& gesture) override;
|
||||
[[nodiscard]] InputSink& input() { return mHost.rawInput(); }
|
||||
[[nodiscard]] Vec2F toContent(Vec2F surfacePoint) const;
|
||||
[[nodiscard]] std::string activationToken();
|
||||
|
||||
PlatformWindowHost& mHost;
|
||||
bool mAcceptsInput = true;
|
||||
bool mPointerOnContent = false;
|
||||
std::unique_ptr<WaylandConnection> mConnection;
|
||||
std::unique_ptr<WaylandSeat> mSeat;
|
||||
int mWakeDescriptor = -1;
|
||||
|
||||
struct Objects;
|
||||
std::unique_ptr<Objects> mObjects;
|
||||
|
||||
WindowFrame mFrame;
|
||||
Vec2F mRestoredContentSize;
|
||||
halnf mScale = 1;
|
||||
bool mConfigured = false;
|
||||
std::optional<WaylandConfigure> mPendingConfigure;
|
||||
bool mCloseRequested = false;
|
||||
bool mMaximized = false;
|
||||
bool mFullScreen = false;
|
||||
bool mActivated = false;
|
||||
bool mGeometryDirty = true;
|
||||
WindowEdge mHoveredEdge = WindowEdge::None;
|
||||
int mHeldButtons = 0;
|
||||
CursorShape mClientCursor = CursorShape::Default;
|
||||
};
|
||||
}
|
||||
433
GraphicApplication/src/canvas/private/Canvas.cpp
Normal file
433
GraphicApplication/src/canvas/private/Canvas.cpp
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
#include "CanvasContext.hpp"
|
||||
|
||||
#include "GpuError.hpp"
|
||||
#include "Profiler.hpp"
|
||||
|
||||
#include <nanovg_vk.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
using namespace tp;
|
||||
|
||||
namespace {
|
||||
constexpr halnf kItalicSkew = -0.21f;
|
||||
|
||||
bool isItalic(FontFace face) { return face == FontFace::Italic || face == FontFace::BoldItalic; }
|
||||
|
||||
void addRect(NVGcontext* vg, const RectF& rect, halnf round) {
|
||||
if (round == 0) {
|
||||
nvgRect(vg, rect.x, rect.y, rect.z, rect.w);
|
||||
} else {
|
||||
nvgRoundedRect(vg, rect.x, rect.y, rect.z, rect.w, round);
|
||||
}
|
||||
}
|
||||
|
||||
void stroke(NVGcontext* vg, const RGBA& color, halnf width) {
|
||||
nvgStrokeWidth(vg, width);
|
||||
nvgStrokeColor(vg, toNVGColor(color));
|
||||
nvgStroke(vg);
|
||||
}
|
||||
|
||||
void fill(NVGcontext* vg, const RGBA& color) {
|
||||
nvgFillColor(vg, toNVGColor(color));
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
NVGpaint linearPaint(NVGcontext* vg, const Canvas::LinearGradient& gradient, Vec2F origin) {
|
||||
const Vec2F from = gradient.from + origin;
|
||||
const Vec2F to = gradient.to + origin;
|
||||
return nvgLinearGradient(
|
||||
vg, from.x, from.y, to.x, to.y, toNVGColor(gradient.fromColor), toNVGColor(gradient.toColor)
|
||||
);
|
||||
}
|
||||
|
||||
int nvgCompositeOperationOf(Canvas::CompositeOperation operation) {
|
||||
switch (operation) {
|
||||
case Canvas::CompositeOperation::SourceOver: return NVG_SOURCE_OVER;
|
||||
case Canvas::CompositeOperation::DestinationOut: return NVG_DESTINATION_OUT;
|
||||
case Canvas::CompositeOperation::DestinationOver: return NVG_DESTINATION_OVER;
|
||||
}
|
||||
return NVG_SOURCE_OVER;
|
||||
}
|
||||
|
||||
int nvgWindingOf(Canvas::PathWinding winding) { return winding == Canvas::PathWinding::Hole ? NVG_HOLE : NVG_SOLID; }
|
||||
}
|
||||
|
||||
Canvas::Canvas(WindowSurface& surface, Profiler& profiler, const std::filesystem::path& resourceDirectory) :
|
||||
mContext(std::make_unique<Context>()) {
|
||||
mContext->surface = &surface;
|
||||
mContext->profiler = &profiler;
|
||||
mContext->flushSection = profiler.section("frame.ui_flush");
|
||||
|
||||
const GpuDevice& gpu = surface.gpu();
|
||||
VKNVGCreateInfo createInfo{};
|
||||
createInfo.gpu = gpu.physicalDevice();
|
||||
createInfo.device = gpu.device();
|
||||
createInfo.renderpass = surface.renderPass();
|
||||
createInfo.cmdBuffer = mContext->frameCommandBuffers;
|
||||
createInfo.frameCount = WindowSurface::kFramesInFlight;
|
||||
createInfo.currentFrame = &mContext->frameSlot;
|
||||
createInfo.commandPool = gpu.commandPool();
|
||||
createInfo.graphicsQueueFamilyIndex = gpu.graphicsQueueFamily();
|
||||
|
||||
mContext->vg = nvgCreateVk(&createInfo, NVG_ANTIALIAS | NVG_STENCIL_STROKES, gpu.graphicsQueue());
|
||||
if (!mContext->vg) {
|
||||
throw GpuError("creating the NanoVG context", VK_ERROR_INITIALIZATION_FAILED);
|
||||
}
|
||||
mContext->fonts = loadCanvasFonts(mContext->vg, resourceDirectory);
|
||||
}
|
||||
|
||||
Canvas::~Canvas() {
|
||||
mContext->surface->gpu().waitIdle();
|
||||
nvgDeleteVk(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::drawBegin(const GpuFrame& frame, const WindowFrame& windowFrame) {
|
||||
mContext->frameCommandBuffers[frame.slot] = frame.commandBuffer;
|
||||
mContext->frameSlot = frame.slot;
|
||||
|
||||
const Vec2F surfaceSize = windowFrame.surfaceSize();
|
||||
const halnf devicePixelRatio = surfaceSize.x > 0 ? (halnf) frame.extent.width / surfaceSize.x : 1.0f;
|
||||
nvgBeginFrame(mContext->vg, surfaceSize.x, surfaceSize.y, devicePixelRatio);
|
||||
nvgTranslate(mContext->vg, windowFrame.margin(), windowFrame.margin());
|
||||
|
||||
mOrigin = { 0, 0 };
|
||||
mClips.clear();
|
||||
mSavedFrames.clear();
|
||||
}
|
||||
|
||||
void Canvas::drawEnd() {
|
||||
const ProfileScope scope(*mContext->profiler, mContext->flushSection);
|
||||
nvgEndFrame(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::save() {
|
||||
nvgSave(mContext->vg);
|
||||
mSavedFrames.append({ mOrigin, mClips.size() });
|
||||
}
|
||||
|
||||
void Canvas::restore() {
|
||||
DEBUG_ASSERT(mSavedFrames.size());
|
||||
const SavedFrame saved = mSavedFrames.last();
|
||||
mSavedFrames.pop();
|
||||
|
||||
while (mClips.size() > saved.clipDepth) {
|
||||
mClips.pop();
|
||||
}
|
||||
mOrigin = saved.origin;
|
||||
nvgRestore(mContext->vg);
|
||||
applyClip();
|
||||
}
|
||||
|
||||
void Canvas::setCompositeOperation(CompositeOperation operation) {
|
||||
nvgGlobalCompositeOperation(mContext->vg, nvgCompositeOperationOf(operation));
|
||||
}
|
||||
|
||||
void Canvas::beginPath() { nvgBeginPath(mContext->vg); }
|
||||
|
||||
void Canvas::addPathRect(RectF rect, halnf round) {
|
||||
rect.pos += mOrigin;
|
||||
addRect(mContext->vg, rect, round);
|
||||
}
|
||||
|
||||
void Canvas::setPathWinding(PathWinding winding) { nvgPathWinding(mContext->vg, nvgWindingOf(winding)); }
|
||||
|
||||
void Canvas::fillPath(const RGBA& color) { fill(mContext->vg, color); }
|
||||
|
||||
void Canvas::fillPath(const BoxGradient& gradient) {
|
||||
const RectF box = { gradient.box.pos + mOrigin, gradient.box.size };
|
||||
const NVGpaint paint = nvgBoxGradient(
|
||||
mContext->vg,
|
||||
box.x,
|
||||
box.y,
|
||||
box.z,
|
||||
box.w,
|
||||
gradient.radius,
|
||||
gradient.feather,
|
||||
toNVGColor(gradient.innerColor),
|
||||
toNVGColor(gradient.outerColor)
|
||||
);
|
||||
nvgFillPaint(mContext->vg, paint);
|
||||
nvgFill(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::rect(RectF rect, const RGBA& color, halnf round) {
|
||||
rect.pos += mOrigin;
|
||||
nvgBeginPath(mContext->vg);
|
||||
addRect(mContext->vg, rect, round);
|
||||
fill(mContext->vg, color);
|
||||
}
|
||||
|
||||
void Canvas::gradientRect(RectF rect, const RGBA& from, const RGBA& to, bool horizontal, halnf round) {
|
||||
rect.pos += mOrigin;
|
||||
NVGcontext* vg = mContext->vg;
|
||||
const halnf endX = horizontal ? rect.x + rect.z : rect.x;
|
||||
const halnf endY = horizontal ? rect.y : rect.y + rect.w;
|
||||
const NVGpaint paint = nvgLinearGradient(vg, rect.x, rect.y, endX, endY, toNVGColor(from), toNVGColor(to));
|
||||
nvgBeginPath(vg);
|
||||
addRect(vg, rect, round);
|
||||
nvgFillPaint(vg, paint);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
void Canvas::debugCross(RectF rect, const RGBA& color) {
|
||||
rect.pos += mOrigin;
|
||||
NVGcontext* vg = mContext->vg;
|
||||
nvgBeginPath(vg);
|
||||
nvgMoveTo(vg, rect.p1().x, rect.p1().y);
|
||||
nvgLineTo(vg, rect.p3().x, rect.p3().y);
|
||||
nvgMoveTo(vg, rect.p2().x, rect.p2().y);
|
||||
nvgLineTo(vg, rect.p4().x, rect.p4().y);
|
||||
stroke(vg, color, 2);
|
||||
}
|
||||
|
||||
void Canvas::frame(RectF rect, const RGBA& color) {
|
||||
rect.pos += mOrigin;
|
||||
NVGcontext* vg = mContext->vg;
|
||||
nvgBeginPath(vg);
|
||||
nvgRect(vg, rect.x, rect.y, rect.z, rect.w);
|
||||
stroke(vg, color, 2);
|
||||
}
|
||||
|
||||
void Canvas::outline(RectF rect, const RGBA& color, halnf round, halnf width) {
|
||||
rect.pos += mOrigin;
|
||||
const halnf inset = width / 2;
|
||||
nvgBeginPath(mContext->vg);
|
||||
nvgRoundedRect(
|
||||
mContext->vg, rect.x + inset, rect.y + inset, rect.z - width, rect.w - width, round > inset ? round - inset : 0
|
||||
);
|
||||
stroke(mContext->vg, color, width);
|
||||
}
|
||||
|
||||
void Canvas::circleOutline(Vec2F center, halnf radius, const RGBA& color, halnf width) {
|
||||
center += mOrigin;
|
||||
nvgBeginPath(mContext->vg);
|
||||
nvgCircle(mContext->vg, center.x, center.y, radius - width / 2);
|
||||
stroke(mContext->vg, color, width);
|
||||
}
|
||||
|
||||
void Canvas::polyline(const Vec2F* points, int count, const RGBA& color, halnf width) {
|
||||
if (count < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
nvgBeginPath(mContext->vg);
|
||||
nvgMoveTo(mContext->vg, points[0].x + mOrigin.x, points[0].y + mOrigin.y);
|
||||
for (int index = 1; index < count; index++) {
|
||||
nvgLineTo(mContext->vg, points[index].x + mOrigin.x, points[index].y + mOrigin.y);
|
||||
}
|
||||
nvgLineCap(mContext->vg, NVG_ROUND);
|
||||
nvgLineJoin(mContext->vg, NVG_ROUND);
|
||||
stroke(mContext->vg, color, width);
|
||||
}
|
||||
|
||||
void Canvas::polygon(const Vec2F* points, int count, const RGBA& color) {
|
||||
if (count < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
nvgBeginPath(mContext->vg);
|
||||
nvgMoveTo(mContext->vg, points[0].x + mOrigin.x, points[0].y + mOrigin.y);
|
||||
for (int index = 1; index < count; index++) {
|
||||
nvgLineTo(mContext->vg, points[index].x + mOrigin.x, points[index].y + mOrigin.y);
|
||||
}
|
||||
nvgClosePath(mContext->vg);
|
||||
fill(mContext->vg, color);
|
||||
}
|
||||
|
||||
void Canvas::polygon(const Vec2F* points, int count, const LinearGradient& gradient) {
|
||||
if (count < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
nvgBeginPath(mContext->vg);
|
||||
nvgMoveTo(mContext->vg, points[0].x + mOrigin.x, points[0].y + mOrigin.y);
|
||||
for (int index = 1; index < count; index++) {
|
||||
nvgLineTo(mContext->vg, points[index].x + mOrigin.x, points[index].y + mOrigin.y);
|
||||
}
|
||||
nvgClosePath(mContext->vg);
|
||||
nvgFillPaint(mContext->vg, linearPaint(mContext->vg, gradient, mOrigin));
|
||||
nvgFill(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::ringSegment(
|
||||
Vec2F center, halnf innerRadius, halnf outerRadius, halnf fromAngle, halnf toAngle, const LinearGradient& gradient
|
||||
) {
|
||||
center += mOrigin;
|
||||
|
||||
nvgBeginPath(mContext->vg);
|
||||
nvgArc(mContext->vg, center.x, center.y, innerRadius, fromAngle, toAngle, NVG_CW);
|
||||
nvgArc(mContext->vg, center.x, center.y, outerRadius, toAngle, fromAngle, NVG_CCW);
|
||||
nvgClosePath(mContext->vg);
|
||||
nvgFillPaint(mContext->vg, linearPaint(mContext->vg, gradient, mOrigin));
|
||||
nvgFill(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::radialGlow(
|
||||
Vec2F center, halnf innerRadius, halnf outerRadius, const RGBA& innerColor, const RGBA& outerColor
|
||||
) {
|
||||
center += mOrigin;
|
||||
|
||||
const NVGpaint paint = nvgRadialGradient(
|
||||
mContext->vg, center.x, center.y, innerRadius, outerRadius, toNVGColor(innerColor), toNVGColor(outerColor)
|
||||
);
|
||||
|
||||
nvgBeginPath(mContext->vg);
|
||||
nvgCircle(mContext->vg, center.x, center.y, outerRadius);
|
||||
nvgFillPaint(mContext->vg, paint);
|
||||
nvgFill(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::line(Vec2F start, Vec2F end, const RGBA& color, halnf thickness) {
|
||||
start += mOrigin;
|
||||
end += mOrigin;
|
||||
nvgBeginPath(mContext->vg);
|
||||
nvgMoveTo(mContext->vg, start.x, start.y);
|
||||
nvgLineTo(mContext->vg, end.x, end.y);
|
||||
stroke(mContext->vg, color, thickness);
|
||||
}
|
||||
|
||||
void Canvas::circle(Vec2F center, halnf radius, const RGBA& color) {
|
||||
center += mOrigin;
|
||||
nvgBeginPath(mContext->vg);
|
||||
nvgCircle(mContext->vg, center.x, center.y, radius);
|
||||
fill(mContext->vg, color);
|
||||
}
|
||||
|
||||
void Canvas::clipTo(const RectF& rect) {
|
||||
const RectF placed = { rect.pos + mOrigin, rect.size };
|
||||
RectF clip = placed;
|
||||
if (mClips.size()) {
|
||||
mClips.last().calcIntersection(placed, clip);
|
||||
}
|
||||
mClips.append(clip);
|
||||
applyClip();
|
||||
}
|
||||
|
||||
void Canvas::applyClip() {
|
||||
if (!mClips.size()) {
|
||||
nvgResetScissor(mContext->vg);
|
||||
return;
|
||||
}
|
||||
const RectF& clip = mClips.last();
|
||||
nvgScissor(mContext->vg, clip.x, clip.y, clip.z, clip.w);
|
||||
}
|
||||
|
||||
bool Canvas::isClippedAway() const { return mClips.size() && mClips.last().size.length2() <= EPSILON; }
|
||||
|
||||
bool Canvas::isOutsideClip(const RectF& rect) const {
|
||||
if (!mClips.size()) {
|
||||
return false;
|
||||
}
|
||||
const RectF placed = { rect.pos + mOrigin, rect.size };
|
||||
const RectF& clip = mClips.last();
|
||||
return placed.x >= clip.x + clip.z || clip.x >= placed.x + placed.z || placed.y >= clip.y + clip.w ||
|
||||
clip.y >= placed.y + placed.w;
|
||||
}
|
||||
|
||||
void Canvas::text(const char* string, const RectF& area, const TextStyle& style) {
|
||||
const halnf padding = style.padding;
|
||||
const RectF padded = { area.x + padding, area.y + padding, area.z - padding * 2, area.w - padding * 2 };
|
||||
if (isOutsideClip(padded)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SavedState saved(*this);
|
||||
clipTo(padded);
|
||||
const RectF rect = { padded.pos + mOrigin, padded.size };
|
||||
|
||||
NVGcontext* vg = mContext->vg;
|
||||
nvgFontSize(vg, style.size);
|
||||
nvgFontFaceId(vg, mContext->fonts.idFor(style.face));
|
||||
nvgFillColor(vg, toNVGColor(style.color));
|
||||
|
||||
halnf anchorX = rect.x;
|
||||
halnf anchorY = rect.y;
|
||||
int alignment = 0;
|
||||
switch (style.align.horizontal) {
|
||||
case HorizontalAlign::Left: alignment |= NVG_ALIGN_LEFT; break;
|
||||
case HorizontalAlign::Center:
|
||||
alignment |= NVG_ALIGN_CENTER;
|
||||
anchorX += rect.z * 0.5f;
|
||||
break;
|
||||
case HorizontalAlign::Right:
|
||||
alignment |= NVG_ALIGN_RIGHT;
|
||||
anchorX += rect.z;
|
||||
break;
|
||||
}
|
||||
switch (style.align.vertical) {
|
||||
case VerticalAlign::Top: alignment |= NVG_ALIGN_TOP; break;
|
||||
case VerticalAlign::Center:
|
||||
alignment |= NVG_ALIGN_MIDDLE;
|
||||
anchorY += rect.w * 0.5f;
|
||||
break;
|
||||
case VerticalAlign::Bottom:
|
||||
alignment |= NVG_ALIGN_BOTTOM;
|
||||
anchorY += rect.w;
|
||||
break;
|
||||
}
|
||||
nvgTextAlign(vg, alignment);
|
||||
|
||||
if (!isItalic(style.face)) {
|
||||
nvgText(vg, anchorX, anchorY, string, nullptr);
|
||||
return;
|
||||
}
|
||||
nvgTranslate(vg, anchorX, anchorY);
|
||||
nvgSkewX(vg, kItalicSkew);
|
||||
nvgText(vg, 0, 0, string, nullptr);
|
||||
}
|
||||
|
||||
void Canvas::rotatedText(const char* string, Vec2F center, halnf size, halnf angle, const RGBA& color) {
|
||||
center += mOrigin;
|
||||
|
||||
nvgSave(mContext->vg);
|
||||
nvgTranslate(mContext->vg, center.x, center.y);
|
||||
nvgRotate(mContext->vg, angle);
|
||||
nvgFontSize(mContext->vg, size);
|
||||
nvgFontFaceId(mContext->vg, mContext->fonts.regular);
|
||||
nvgTextAlign(mContext->vg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
|
||||
nvgFillColor(mContext->vg, toNVGColor(color));
|
||||
nvgText(mContext->vg, 0, 0, string, nullptr);
|
||||
nvgRestore(mContext->vg);
|
||||
}
|
||||
|
||||
halnf Canvas::textWidth(const char* string, halnf size, FontFace face) {
|
||||
nvgFontSize(mContext->vg, size);
|
||||
nvgFontFaceId(mContext->vg, mContext->fonts.idFor(face));
|
||||
return nvgTextBounds(mContext->vg, 0, 0, string, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void Canvas::prefixWidths(const char* string, size_t length, halnf size, FontFace face, halnf* widths) {
|
||||
nvgFontSize(mContext->vg, size);
|
||||
nvgFontFaceId(mContext->vg, mContext->fonts.idFor(face));
|
||||
nvgTextAlign(mContext->vg, NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
|
||||
|
||||
std::vector<NVGglyphPosition> glyphs(length);
|
||||
const int count = nvgTextGlyphPositions(mContext->vg, 0, 0, string, string + length, glyphs.data(), (int) length);
|
||||
|
||||
size_t byte = 0;
|
||||
for (int index = 0; index < count; index++) {
|
||||
const size_t glyphStart = (size_t) (glyphs[(size_t) index].str - string);
|
||||
const halnf pen = glyphs[(size_t) index].x;
|
||||
for (; byte <= glyphStart && byte < length; byte++) {
|
||||
widths[byte] = pen;
|
||||
}
|
||||
}
|
||||
const halnf total = nvgTextBounds(mContext->vg, 0, 0, string, string + length, nullptr);
|
||||
for (; byte < length; byte++) {
|
||||
widths[byte] = total;
|
||||
}
|
||||
widths[length] = total;
|
||||
}
|
||||
|
||||
void Canvas::drawImage(RectF rect, ImageHandle image, halnf angle, halnf alpha, halnf rounding) {
|
||||
if (!image.isValid()) {
|
||||
return;
|
||||
}
|
||||
rect.pos += mOrigin;
|
||||
const NVGpaint paint = nvgImagePattern(mContext->vg, rect.x, rect.y, rect.z, rect.w, angle, image.mId, alpha);
|
||||
nvgBeginPath(mContext->vg);
|
||||
nvgRoundedRect(mContext->vg, rect.x, rect.y, rect.z, rect.w, rounding);
|
||||
nvgFillPaint(mContext->vg, paint);
|
||||
nvgFill(mContext->vg);
|
||||
}
|
||||
24
GraphicApplication/src/canvas/private/CanvasContext.hpp
Normal file
24
GraphicApplication/src/canvas/private/CanvasContext.hpp
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
|
||||
#include "Canvas.hpp"
|
||||
#include "CanvasFontLoader.hpp"
|
||||
|
||||
#include <nanovg.h>
|
||||
|
||||
namespace tp {
|
||||
|
||||
class Canvas::Context {
|
||||
public:
|
||||
NVGcontext* vg{};
|
||||
WindowSurface* surface{};
|
||||
Profiler* profiler{};
|
||||
uint32_t flushSection = 0;
|
||||
CanvasFontIds fonts;
|
||||
|
||||
// NanoVG reads the recording command buffer and frame slot through these pointers on every flush.
|
||||
VkCommandBuffer frameCommandBuffers[WindowSurface::kFramesInFlight] = {};
|
||||
uint32_t frameSlot = 0;
|
||||
};
|
||||
|
||||
inline NVGcolor toNVGColor(const RGBA& color) { return { color.r, color.g, color.b, color.a }; }
|
||||
}
|
||||
89
GraphicApplication/src/canvas/private/CanvasFontLoader.cpp
Normal file
89
GraphicApplication/src/canvas/private/CanvasFontLoader.cpp
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#include "CanvasFontLoader.hpp"
|
||||
#include "CanvasFonts.hpp"
|
||||
|
||||
#include <nanovg.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace tp;
|
||||
|
||||
namespace {
|
||||
bool isBold(FontFace face) { return face == FontFace::Bold || face == FontFace::BoldItalic; }
|
||||
|
||||
class LoadedFallbackFonts {
|
||||
public:
|
||||
LoadedFallbackFonts(NVGcontext* vg, const std::filesystem::path& resourceDirectory) {
|
||||
for (const FontFile& font : kFallbackFonts) {
|
||||
const std::string path = (resourceDirectory / font.path).string();
|
||||
mLoaded.emplace_back(font.name, nvgCreateFont(vg, font.name, path.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
template <size_t Size>
|
||||
[[nodiscard]] std::vector<int> idsInChainOrder(const std::array<FontFile, Size>& chain) const {
|
||||
std::vector<int> ids;
|
||||
for (const FontFile& font : chain) {
|
||||
const int id = idOf(font.name);
|
||||
if (id != -1) {
|
||||
ids.push_back(id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] int idOf(const char* name) const {
|
||||
for (const auto& [loaded, id] : mLoaded) {
|
||||
if (std::strcmp(loaded, name) == 0) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::vector<std::pair<const char*, int>> mLoaded;
|
||||
};
|
||||
|
||||
int loadTextFont(
|
||||
NVGcontext* vg,
|
||||
const FontFile& font,
|
||||
const std::vector<int>& fallbacks,
|
||||
int missing,
|
||||
const std::filesystem::path& resourceDirectory
|
||||
) {
|
||||
const std::string path = (resourceDirectory / font.path).string();
|
||||
const int id = nvgCreateFont(vg, font.name, path.c_str());
|
||||
if (id == -1) {
|
||||
return missing;
|
||||
}
|
||||
for (const int fallback : fallbacks) {
|
||||
nvgAddFallbackFontId(vg, id, fallback);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
int CanvasFontIds::idFor(FontFace face) const {
|
||||
if (face == FontFace::Monospace) {
|
||||
return monospace;
|
||||
}
|
||||
return isBold(face) ? bold : regular;
|
||||
}
|
||||
|
||||
CanvasFontIds tp::loadCanvasFonts(NVGcontext* vg, const std::filesystem::path& resourceDirectory) {
|
||||
const LoadedFallbackFonts fallbacks(vg, resourceDirectory);
|
||||
|
||||
CanvasFontIds fonts;
|
||||
fonts.regular = loadTextFont(vg, kRegularFont, fallbacks.idsInChainOrder(kRegularFallbacks), -1, resourceDirectory);
|
||||
if (fonts.regular == -1) {
|
||||
throw std::runtime_error("cannot load " + (resourceDirectory / kRegularFont.path).string());
|
||||
}
|
||||
fonts.bold = loadTextFont(vg, kBoldFont, fallbacks.idsInChainOrder(kBoldFallbacks), fonts.regular, resourceDirectory);
|
||||
fonts.monospace =
|
||||
loadTextFont(vg, kMonospaceFont, fallbacks.idsInChainOrder(kMonospaceFallbacks), fonts.regular, resourceDirectory);
|
||||
return fonts;
|
||||
}
|
||||
20
GraphicApplication/src/canvas/private/CanvasFontLoader.hpp
Normal file
20
GraphicApplication/src/canvas/private/CanvasFontLoader.hpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
|
||||
#include "Canvas.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
struct NVGcontext;
|
||||
|
||||
namespace tp {
|
||||
|
||||
struct CanvasFontIds {
|
||||
int regular = -1;
|
||||
int bold = -1;
|
||||
int monospace = -1;
|
||||
|
||||
[[nodiscard]] int idFor(FontFace face) const;
|
||||
};
|
||||
|
||||
CanvasFontIds loadCanvasFonts(NVGcontext* vg, const std::filesystem::path& resourceDirectory);
|
||||
}
|
||||
47
GraphicApplication/src/canvas/private/CanvasFonts.hpp
Normal file
47
GraphicApplication/src/canvas/private/CanvasFonts.hpp
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
|
||||
namespace tp {
|
||||
|
||||
struct FontFile {
|
||||
const char* name;
|
||||
const char* path;
|
||||
};
|
||||
|
||||
inline constexpr FontFile kRegularFont = { "default", "Font.ttf" };
|
||||
inline constexpr FontFile kBoldFont = { "bold", "FontBold.ttf" };
|
||||
inline constexpr FontFile kMonospaceFont = { "monospace", "FontMono.ttf" };
|
||||
|
||||
inline constexpr std::array kTextFonts = { kRegularFont, kBoldFont, kMonospaceFont };
|
||||
|
||||
inline constexpr FontFile kEmojiFont = { "emoji", "Emoji.ttf" };
|
||||
inline constexpr FontFile kSymbolsFont = { "symbols", "FontSymbols.ttf" };
|
||||
inline constexpr FontFile kMathFont = { "math", "FontMath.ttf" };
|
||||
inline constexpr FontFile kSpecialsFont = { "specials", "FontSpecials.ttf" };
|
||||
|
||||
inline constexpr FontFile kCyrillicRegularFont = { "cyrillic", "FontCyrillic.ttf" };
|
||||
inline constexpr FontFile kCyrillicBoldFont = { "cyrillic-bold", "FontCyrillicBold.ttf" };
|
||||
|
||||
inline constexpr std::array kFallbackFonts = {
|
||||
kEmojiFont, kSymbolsFont, kMathFont, kSpecialsFont, kCyrillicRegularFont, kCyrillicBoldFont,
|
||||
};
|
||||
|
||||
inline constexpr std::array kSymbolFallbacksFirst = { kEmojiFont, kSymbolsFont, kMathFont, kSpecialsFont };
|
||||
|
||||
template <std::size_t Size>
|
||||
constexpr std::array<FontFile, Size + 1>
|
||||
symbolsThenCyrillic(const std::array<FontFile, Size>& symbols, const FontFile& cyrillicOfFaceWeight) {
|
||||
std::array<FontFile, Size + 1> chain{};
|
||||
for (std::size_t index = 0; index < Size; index++) {
|
||||
chain[index] = symbols[index];
|
||||
}
|
||||
chain[Size] = cyrillicOfFaceWeight;
|
||||
return chain;
|
||||
}
|
||||
|
||||
inline constexpr std::array kRegularFallbacks = symbolsThenCyrillic(kSymbolFallbacksFirst, kCyrillicRegularFont);
|
||||
inline constexpr std::array kBoldFallbacks = symbolsThenCyrillic(kSymbolFallbacksFirst, kCyrillicBoldFont);
|
||||
inline constexpr std::array kMonospaceFallbacks = symbolsThenCyrillic(kSymbolFallbacksFirst, kCyrillicRegularFont);
|
||||
}
|
||||
70
GraphicApplication/src/canvas/private/CanvasImages.cpp
Normal file
70
GraphicApplication/src/canvas/private/CanvasImages.cpp
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
#include "CanvasContext.hpp"
|
||||
|
||||
#include "RenderTargetRegistry.hpp"
|
||||
|
||||
#include <nanovg_vk.h>
|
||||
|
||||
using namespace tp;
|
||||
|
||||
ImageHandle Canvas::createImageFromRenderTarget(uint32_t renderTargetId) {
|
||||
RenderTargetEntry entry;
|
||||
if (!mContext->surface->gpu().renderTargets().resolve(renderTargetId, entry)) {
|
||||
return {};
|
||||
}
|
||||
const int image = nvgCreateImageFromHandleVk(
|
||||
mContext->vg,
|
||||
entry.image,
|
||||
entry.view,
|
||||
entry.sampler,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
entry.width,
|
||||
entry.height,
|
||||
0
|
||||
);
|
||||
return ImageHandle(image);
|
||||
}
|
||||
|
||||
void Canvas::updateImageFromRenderTarget(ImageHandle image, uint32_t renderTargetId) {
|
||||
RenderTargetEntry entry;
|
||||
if (!image.isValid() || !mContext->surface->gpu().renderTargets().resolve(renderTargetId, entry)) {
|
||||
return;
|
||||
}
|
||||
nvgUpdateImageFromHandleVk(
|
||||
mContext->vg,
|
||||
image.mId,
|
||||
entry.image,
|
||||
entry.view,
|
||||
entry.sampler,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
entry.width,
|
||||
entry.height
|
||||
);
|
||||
}
|
||||
|
||||
ImageHandle Canvas::createImageFromFile(const char* path) { return ImageHandle(nvgCreateImage(mContext->vg, path, 0)); }
|
||||
|
||||
ImageHandle Canvas::createImageFromMemoryRGBA(int width, int height, const unsigned char* rgba) {
|
||||
return ImageHandle(nvgCreateImageRGBA(mContext->vg, width, height, 0, rgba));
|
||||
}
|
||||
|
||||
void Canvas::updateImageRGBA(ImageHandle image, const unsigned char* rgba) {
|
||||
if (image.isValid()) {
|
||||
nvgUpdateImage(mContext->vg, image.mId, rgba);
|
||||
}
|
||||
}
|
||||
|
||||
Vec2F Canvas::imageSize(ImageHandle image) {
|
||||
if (!image.isValid()) {
|
||||
return { 0, 0 };
|
||||
}
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
nvgImageSize(mContext->vg, image.mId, &width, &height);
|
||||
return { (halnf) width, (halnf) height };
|
||||
}
|
||||
|
||||
void Canvas::deleteImage(ImageHandle image) {
|
||||
if (image.isValid()) {
|
||||
nvgDeleteImage(mContext->vg, image.mId);
|
||||
}
|
||||
}
|
||||
183
GraphicApplication/src/canvas/public/Canvas.hpp
Normal file
183
GraphicApplication/src/canvas/public/Canvas.hpp
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
#pragma once
|
||||
|
||||
#include "Color.hpp"
|
||||
#include "Rect.hpp"
|
||||
#include "WindowFrame.hpp"
|
||||
#include "WindowSurface.hpp"
|
||||
|
||||
#include "Buffer.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
namespace tp {
|
||||
|
||||
class Profiler;
|
||||
|
||||
enum class HorizontalAlign { Left, Center, Right };
|
||||
|
||||
enum class VerticalAlign { Top, Center, Bottom };
|
||||
|
||||
struct TextAlign {
|
||||
HorizontalAlign horizontal = HorizontalAlign::Left;
|
||||
VerticalAlign vertical = VerticalAlign::Center;
|
||||
|
||||
static constexpr TextAlign leftCenter() { return { HorizontalAlign::Left, VerticalAlign::Center }; }
|
||||
static constexpr TextAlign centered() { return { HorizontalAlign::Center, VerticalAlign::Center }; }
|
||||
};
|
||||
|
||||
enum class FontFace { Regular, Bold, Italic, BoldItalic, Monospace };
|
||||
|
||||
struct TextStyle {
|
||||
halnf size = 16;
|
||||
RGBA color = RGBA(1, 1, 1, 1);
|
||||
TextAlign align;
|
||||
halnf padding = 0;
|
||||
FontFace face = FontFace::Regular;
|
||||
};
|
||||
|
||||
class ImageHandle {
|
||||
public:
|
||||
ImageHandle() = default;
|
||||
|
||||
[[nodiscard]] bool isValid() const { return mId > 0; }
|
||||
[[nodiscard]] bool operator==(const ImageHandle& other) const = default;
|
||||
|
||||
private:
|
||||
friend class Canvas;
|
||||
explicit ImageHandle(int id) :
|
||||
mId(id) {}
|
||||
|
||||
int mId = 0;
|
||||
};
|
||||
|
||||
// 2D vector drawing for one window surface. Coordinates are relative to the current placement origin, which
|
||||
// starts at the content origin; placements and clips are scoped.
|
||||
class Canvas {
|
||||
class Context;
|
||||
|
||||
public:
|
||||
Canvas(WindowSurface& surface, Profiler& profiler, const std::filesystem::path& resourceDirectory);
|
||||
~Canvas();
|
||||
|
||||
Canvas(const Canvas&) = delete;
|
||||
Canvas& operator=(const Canvas&) = delete;
|
||||
|
||||
void drawBegin(const GpuFrame& frame, const WindowFrame& windowFrame);
|
||||
void drawEnd();
|
||||
|
||||
enum class CompositeOperation { SourceOver, DestinationOut, DestinationOver };
|
||||
|
||||
enum class PathWinding { Solid, Hole };
|
||||
|
||||
struct LinearGradient {
|
||||
Vec2F from;
|
||||
Vec2F to;
|
||||
RGBA fromColor;
|
||||
RGBA toColor;
|
||||
};
|
||||
|
||||
struct BoxGradient {
|
||||
RectF box;
|
||||
halnf radius = 0;
|
||||
halnf feather = 0;
|
||||
RGBA innerColor;
|
||||
RGBA outerColor;
|
||||
};
|
||||
|
||||
// Restores origin, clipping and composite operation when the scope ends.
|
||||
class SavedState {
|
||||
public:
|
||||
explicit SavedState(Canvas& canvas) :
|
||||
mCanvas(canvas) {
|
||||
mCanvas.save();
|
||||
}
|
||||
|
||||
~SavedState() { mCanvas.restore(); }
|
||||
|
||||
SavedState(const SavedState&) = delete;
|
||||
SavedState& operator=(const SavedState&) = delete;
|
||||
|
||||
protected:
|
||||
Canvas& mCanvas;
|
||||
};
|
||||
|
||||
// Moves the origin by an offset from the current origin until destroyed.
|
||||
class Translation : public SavedState {
|
||||
public:
|
||||
Translation(Canvas& canvas, Vec2F offset) :
|
||||
SavedState(canvas) {
|
||||
mCanvas.translateOrigin(offset);
|
||||
}
|
||||
};
|
||||
|
||||
// Places the origin at a point relative to the current origin and clips to an area there, until destroyed.
|
||||
class Placement : public SavedState {
|
||||
public:
|
||||
Placement(Canvas& canvas, Vec2F origin, Vec2F clipSize) :
|
||||
SavedState(canvas) {
|
||||
mCanvas.translateOrigin(origin);
|
||||
mCanvas.clipTo({ { 0, 0 }, clipSize });
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] Vec2F origin() const { return mOrigin; }
|
||||
[[nodiscard]] bool isClippedAway() const;
|
||||
[[nodiscard]] bool isOutsideClip(const RectF& rect) const;
|
||||
|
||||
void setCompositeOperation(CompositeOperation operation);
|
||||
|
||||
void beginPath();
|
||||
void addPathRect(RectF rect, halnf round = 0);
|
||||
void setPathWinding(PathWinding winding);
|
||||
void fillPath(const RGBA& color);
|
||||
void fillPath(const BoxGradient& gradient);
|
||||
|
||||
void rect(RectF rect, const RGBA& color, halnf round = 0);
|
||||
void gradientRect(RectF rect, const RGBA& from, const RGBA& to, bool horizontal, halnf round = 0);
|
||||
void frame(RectF rect, const RGBA& color);
|
||||
void debugCross(RectF rect, const RGBA& color);
|
||||
void outline(RectF rect, const RGBA& color, halnf round, halnf width);
|
||||
void circle(Vec2F center, halnf radius, const RGBA& color);
|
||||
void circleOutline(Vec2F center, halnf radius, const RGBA& color, halnf width);
|
||||
void line(Vec2F start, Vec2F end, const RGBA& color, halnf thickness);
|
||||
void polyline(const Vec2F* points, int count, const RGBA& color, halnf width);
|
||||
void polygon(const Vec2F* points, int count, const RGBA& color);
|
||||
void polygon(const Vec2F* points, int count, const LinearGradient& gradient);
|
||||
void ringSegment(
|
||||
Vec2F center, halnf innerRadius, halnf outerRadius, halnf fromAngle, halnf toAngle, const LinearGradient& gradient
|
||||
);
|
||||
void radialGlow(Vec2F center, halnf innerRadius, halnf outerRadius, const RGBA& innerColor, const RGBA& outerColor);
|
||||
|
||||
void text(const char* text, const RectF& area, const TextStyle& style);
|
||||
void rotatedText(const char* text, Vec2F center, halnf size, halnf angle, const RGBA& color);
|
||||
[[nodiscard]] halnf textWidth(const char* text, halnf size, FontFace face = FontFace::Regular);
|
||||
void prefixWidths(const char* text, size_t length, halnf size, FontFace face, halnf* widths);
|
||||
|
||||
[[nodiscard]] ImageHandle createImageFromRenderTarget(uint32_t renderTargetId);
|
||||
void updateImageFromRenderTarget(ImageHandle image, uint32_t renderTargetId);
|
||||
[[nodiscard]] ImageHandle createImageFromFile(const char* path);
|
||||
[[nodiscard]] ImageHandle createImageFromMemoryRGBA(int width, int height, const unsigned char* rgba);
|
||||
void updateImageRGBA(ImageHandle image, const unsigned char* rgba);
|
||||
[[nodiscard]] Vec2F imageSize(ImageHandle image);
|
||||
void deleteImage(ImageHandle image);
|
||||
void drawImage(RectF rect, ImageHandle image, halnf angle = 0, halnf alpha = 1, halnf rounding = 0);
|
||||
|
||||
private:
|
||||
struct SavedFrame {
|
||||
Vec2F origin;
|
||||
size_t clipDepth = 0;
|
||||
};
|
||||
|
||||
void save();
|
||||
void restore();
|
||||
void translateOrigin(Vec2F offset) { mOrigin += offset; }
|
||||
void clipTo(const RectF& rect);
|
||||
void applyClip();
|
||||
|
||||
std::unique_ptr<Context> mContext;
|
||||
Buffer<RectF> mClips;
|
||||
Buffer<SavedFrame> mSavedFrames;
|
||||
Vec2F mOrigin = { 0, 0 };
|
||||
};
|
||||
}
|
||||
126
GraphicApplication/src/debug_ui/private/DebugGUI.cpp
Normal file
126
GraphicApplication/src/debug_ui/private/DebugGUI.cpp
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
#include "DebugGUI.hpp"
|
||||
|
||||
#include "GpuError.hpp"
|
||||
#include "ImGuiPlatform.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_impl_vulkan.h>
|
||||
|
||||
#include "implot.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace tp {
|
||||
namespace {
|
||||
constexpr float kFontSize = 18.f;
|
||||
constexpr float kHeadlessFrameSeconds = 1.0f / 60.0f;
|
||||
|
||||
VkDescriptorPool createDescriptorPool(VkDevice device) {
|
||||
const std::array<VkDescriptorPoolSize, 1> poolSizes = {
|
||||
VkDescriptorPoolSize{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 64 },
|
||||
};
|
||||
const VkDescriptorPoolCreateInfo poolInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
|
||||
.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT,
|
||||
.maxSets = 64,
|
||||
.poolSizeCount = (uint32_t) poolSizes.size(),
|
||||
.pPoolSizes = poolSizes.data(),
|
||||
};
|
||||
VkDescriptorPool pool = VK_NULL_HANDLE;
|
||||
requireSuccess(vkCreateDescriptorPool(device, &poolInfo, nullptr, &pool), "vkCreateDescriptorPool");
|
||||
return pool;
|
||||
}
|
||||
}
|
||||
|
||||
class DebugGUI::Context {
|
||||
public:
|
||||
Context(Window& window, DebugInterface mode) :
|
||||
window(window),
|
||||
mode(mode) {}
|
||||
|
||||
Window& window;
|
||||
DebugInterface mode;
|
||||
ImGuiContext* imgui = nullptr;
|
||||
ImPlotContext* implot = nullptr;
|
||||
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
|
||||
std::unique_ptr<ImGuiPlatform> platform;
|
||||
|
||||
[[nodiscard]] bool isVisible() const { return mode == DebugInterface::Visible; }
|
||||
};
|
||||
}
|
||||
|
||||
using namespace tp;
|
||||
|
||||
DebugGUI::DebugGUI(Window& window, const std::filesystem::path& resourceDirectory, DebugInterface mode) :
|
||||
mContext(std::make_unique<Context>(window, mode)) {
|
||||
IMGUI_CHECKVERSION();
|
||||
mContext->imgui = ImGui::CreateContext();
|
||||
mContext->implot = ImPlot::CreateContext();
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.ConfigInputTrickleEventQueue = false;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
io.IniFilename = nullptr;
|
||||
const std::string fontPath = (resourceDirectory / "Font.ttf").string();
|
||||
io.Fonts->AddFontFromFileTTF(fontPath.c_str(), kFontSize);
|
||||
|
||||
if (!mContext->isVisible()) {
|
||||
io.Fonts->Build();
|
||||
return;
|
||||
}
|
||||
|
||||
WindowSurface& surface = window.surface();
|
||||
const GpuDevice& gpu = surface.gpu();
|
||||
mContext->platform = std::make_unique<ImGuiPlatform>(window);
|
||||
mContext->descriptorPool = createDescriptorPool(gpu.device());
|
||||
|
||||
ImGui_ImplVulkan_InitInfo initInfo{};
|
||||
initInfo.Instance = gpu.instance();
|
||||
initInfo.PhysicalDevice = gpu.physicalDevice();
|
||||
initInfo.Device = gpu.device();
|
||||
initInfo.QueueFamily = gpu.graphicsQueueFamily();
|
||||
initInfo.Queue = gpu.graphicsQueue();
|
||||
initInfo.DescriptorPool = mContext->descriptorPool;
|
||||
initInfo.RenderPass = surface.renderPass();
|
||||
initInfo.MinImageCount = WindowSurface::kFramesInFlight;
|
||||
initInfo.ImageCount = std::max(surface.imageCount(), WindowSurface::kFramesInFlight);
|
||||
initInfo.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
ImGui_ImplVulkan_Init(&initInfo);
|
||||
}
|
||||
|
||||
DebugGUI::~DebugGUI() {
|
||||
if (mContext->isVisible()) {
|
||||
mContext->window.surface().gpu().waitIdle();
|
||||
ImGui_ImplVulkan_Shutdown();
|
||||
mContext->platform.reset();
|
||||
vkDestroyDescriptorPool(mContext->window.surface().gpu().device(), mContext->descriptorPool, nullptr);
|
||||
}
|
||||
ImPlot::DestroyContext(mContext->implot);
|
||||
ImGui::DestroyContext(mContext->imgui);
|
||||
}
|
||||
|
||||
DebugInterface DebugGUI::mode() const { return mContext->mode; }
|
||||
|
||||
void DebugGUI::drawBegin() {
|
||||
if (mContext->isVisible()) {
|
||||
ImGui_ImplVulkan_NewFrame();
|
||||
mContext->platform->newFrame();
|
||||
} else {
|
||||
const Vec2F size = mContext->window.contentSize();
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.DisplaySize = ImVec2(std::max(size.x, 1.0f), std::max(size.y, 1.0f));
|
||||
io.DeltaTime = kHeadlessFrameSeconds;
|
||||
}
|
||||
ImGui::NewFrame();
|
||||
}
|
||||
|
||||
void DebugGUI::drawEnd(const GpuFrame& frame) {
|
||||
ImGui::Render();
|
||||
if (!mContext->isVisible()) {
|
||||
return;
|
||||
}
|
||||
mContext->platform->endFrame();
|
||||
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), frame.commandBuffer);
|
||||
}
|
||||
|
||||
void DebugGUI::showValue(const char* label, halnf value) { ImGui::Text("%s: %f", label, value); }
|
||||
280
GraphicApplication/src/debug_ui/private/ImGuiPlatform.cpp
Normal file
280
GraphicApplication/src/debug_ui/private/ImGuiPlatform.cpp
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
#include "ImGuiPlatform.hpp"
|
||||
|
||||
#include "Timing.hpp"
|
||||
#include "Window.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cfloat>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace tp;
|
||||
|
||||
namespace {
|
||||
constexpr double kFirstFrameSeconds = 1.0 / 60.0;
|
||||
|
||||
ImGuiKey imguiKey(Key key) {
|
||||
static const std::unordered_map<Key, ImGuiKey> keys = {
|
||||
{ Key::Tab, ImGuiKey_Tab },
|
||||
{ Key::Left, ImGuiKey_LeftArrow },
|
||||
{ Key::Right, ImGuiKey_RightArrow },
|
||||
{ Key::Up, ImGuiKey_UpArrow },
|
||||
{ Key::Down, ImGuiKey_DownArrow },
|
||||
{ Key::PageUp, ImGuiKey_PageUp },
|
||||
{ Key::PageDown, ImGuiKey_PageDown },
|
||||
{ Key::Home, ImGuiKey_Home },
|
||||
{ Key::End, ImGuiKey_End },
|
||||
{ Key::Insert, ImGuiKey_Insert },
|
||||
{ Key::Delete, ImGuiKey_Delete },
|
||||
{ Key::Backspace, ImGuiKey_Backspace },
|
||||
{ Key::Space, ImGuiKey_Space },
|
||||
{ Key::Enter, ImGuiKey_Enter },
|
||||
{ Key::Escape, ImGuiKey_Escape },
|
||||
{ Key::LeftControl, ImGuiKey_LeftCtrl },
|
||||
{ Key::LeftShift, ImGuiKey_LeftShift },
|
||||
{ Key::LeftAlt, ImGuiKey_LeftAlt },
|
||||
{ Key::LeftSuper, ImGuiKey_LeftSuper },
|
||||
{ Key::RightControl, ImGuiKey_RightCtrl },
|
||||
{ Key::RightShift, ImGuiKey_RightShift },
|
||||
{ Key::RightAlt, ImGuiKey_RightAlt },
|
||||
{ Key::RightSuper, ImGuiKey_RightSuper },
|
||||
{ Key::Menu, ImGuiKey_Menu },
|
||||
{ Key::Apostrophe, ImGuiKey_Apostrophe },
|
||||
{ Key::Comma, ImGuiKey_Comma },
|
||||
{ Key::Minus, ImGuiKey_Minus },
|
||||
{ Key::Period, ImGuiKey_Period },
|
||||
{ Key::Slash, ImGuiKey_Slash },
|
||||
{ Key::Semicolon, ImGuiKey_Semicolon },
|
||||
{ Key::Equal, ImGuiKey_Equal },
|
||||
{ Key::LeftBracket, ImGuiKey_LeftBracket },
|
||||
{ Key::Backslash, ImGuiKey_Backslash },
|
||||
{ Key::RightBracket, ImGuiKey_RightBracket },
|
||||
{ Key::GraveAccent, ImGuiKey_GraveAccent },
|
||||
{ Key::CapsLock, ImGuiKey_CapsLock },
|
||||
{ Key::ScrollLock, ImGuiKey_ScrollLock },
|
||||
{ Key::NumLock, ImGuiKey_NumLock },
|
||||
{ Key::PrintScreen, ImGuiKey_PrintScreen },
|
||||
{ Key::Pause, ImGuiKey_Pause },
|
||||
{ Key::KeypadDecimal, ImGuiKey_KeypadDecimal },
|
||||
{ Key::KeypadDivide, ImGuiKey_KeypadDivide },
|
||||
{ Key::KeypadMultiply, ImGuiKey_KeypadMultiply },
|
||||
{ Key::KeypadSubtract, ImGuiKey_KeypadSubtract },
|
||||
{ Key::KeypadAdd, ImGuiKey_KeypadAdd },
|
||||
{ Key::KeypadEnter, ImGuiKey_KeypadEnter },
|
||||
{ Key::KeypadEqual, ImGuiKey_KeypadEqual },
|
||||
};
|
||||
|
||||
const auto found = keys.find(key);
|
||||
if (found != keys.end()) {
|
||||
return found->second;
|
||||
}
|
||||
|
||||
const auto code = (int) key;
|
||||
if (code >= (int) Key::A && code <= (int) Key::Z) {
|
||||
return (ImGuiKey) (ImGuiKey_A + code - (int) Key::A);
|
||||
}
|
||||
if (code >= (int) Key::Digit0 && code <= (int) Key::Digit9) {
|
||||
return (ImGuiKey) (ImGuiKey_0 + code - (int) Key::Digit0);
|
||||
}
|
||||
if (code >= (int) Key::F1 && code <= (int) Key::F24) {
|
||||
return (ImGuiKey) (ImGuiKey_F1 + code - (int) Key::F1);
|
||||
}
|
||||
if (code >= (int) Key::Keypad0 && code <= (int) Key::Keypad9) {
|
||||
return (ImGuiKey) (ImGuiKey_Keypad0 + code - (int) Key::Keypad0);
|
||||
}
|
||||
return ImGuiKey_None;
|
||||
}
|
||||
|
||||
ImGuiKey imguiModifier(Key key) {
|
||||
switch (key) {
|
||||
case Key::LeftControl:
|
||||
case Key::RightControl: return ImGuiMod_Ctrl;
|
||||
case Key::LeftShift:
|
||||
case Key::RightShift: return ImGuiMod_Shift;
|
||||
case Key::LeftAlt:
|
||||
case Key::RightAlt: return ImGuiMod_Alt;
|
||||
case Key::LeftSuper:
|
||||
case Key::RightSuper: return ImGuiMod_Super;
|
||||
default: return ImGuiKey_None;
|
||||
}
|
||||
}
|
||||
|
||||
bool isRightModifier(Key key) {
|
||||
return key == Key::RightControl || key == Key::RightShift || key == Key::RightAlt || key == Key::RightSuper;
|
||||
}
|
||||
|
||||
int modifierIndex(ImGuiKey modifier) {
|
||||
switch (modifier) {
|
||||
case ImGuiMod_Ctrl: return 0;
|
||||
case ImGuiMod_Shift: return 1;
|
||||
case ImGuiMod_Alt: return 2;
|
||||
default: return 3;
|
||||
}
|
||||
}
|
||||
|
||||
int imguiMouseButton(PointerButton button) {
|
||||
switch (button) {
|
||||
case PointerButton::Left: return ImGuiMouseButton_Left;
|
||||
case PointerButton::Right: return ImGuiMouseButton_Right;
|
||||
case PointerButton::Middle: return ImGuiMouseButton_Middle;
|
||||
case PointerButton::Back: return 3;
|
||||
case PointerButton::Forward: return 4;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
CursorShape cursorShape(ImGuiMouseCursor cursor) {
|
||||
switch (cursor) {
|
||||
case ImGuiMouseCursor_TextInput: return CursorShape::Text;
|
||||
case ImGuiMouseCursor_ResizeAll: return CursorShape::Move;
|
||||
case ImGuiMouseCursor_ResizeNS: return CursorShape::ResizeVertical;
|
||||
case ImGuiMouseCursor_ResizeEW: return CursorShape::ResizeHorizontal;
|
||||
case ImGuiMouseCursor_ResizeNESW: return CursorShape::ResizeNorthEastSouthWest;
|
||||
case ImGuiMouseCursor_ResizeNWSE: return CursorShape::ResizeNorthWestSouthEast;
|
||||
case ImGuiMouseCursor_Hand: return CursorShape::Pointer;
|
||||
case ImGuiMouseCursor_NotAllowed: return CursorShape::NotAllowed;
|
||||
default: return CursorShape::Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGuiPlatform::ImGuiPlatform(Window& window) :
|
||||
mWindow(window) {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.BackendPlatformName = "graphic_application";
|
||||
io.BackendPlatformUserData = this;
|
||||
io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors;
|
||||
io.SetClipboardTextFn = &ImGuiPlatform::writeClipboard;
|
||||
io.GetClipboardTextFn = &ImGuiPlatform::readClipboard;
|
||||
io.ClipboardUserData = this;
|
||||
io.SetPlatformImeDataFn = &ImGuiPlatform::placeInputMethod;
|
||||
}
|
||||
|
||||
ImGuiPlatform::~ImGuiPlatform() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.BackendPlatformName = nullptr;
|
||||
io.BackendPlatformUserData = nullptr;
|
||||
io.SetClipboardTextFn = nullptr;
|
||||
io.GetClipboardTextFn = nullptr;
|
||||
io.SetPlatformImeDataFn = nullptr;
|
||||
}
|
||||
|
||||
void ImGuiPlatform::newFrame() {
|
||||
consumeInput(mWindow.input().state());
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
const Vec2F size = mWindow.contentSize();
|
||||
const Vec2F framebuffer = mWindow.framebufferSize();
|
||||
const Vec2F surface = mWindow.frame().surfaceSize();
|
||||
const halnf scale = surface.x > 0 ? framebuffer.x / surface.x : 1;
|
||||
|
||||
io.DisplaySize = ImVec2(size.x, size.y);
|
||||
io.DisplayFramebufferScale = ImVec2(scale, scale);
|
||||
|
||||
const double now = (double) get_time_ns() / 1.0e9;
|
||||
io.DeltaTime = mPreviousTime > 0 && now > mPreviousTime ? (float) (now - mPreviousTime) : (float) kFirstFrameSeconds;
|
||||
mPreviousTime = now;
|
||||
|
||||
updateCursor();
|
||||
}
|
||||
|
||||
void ImGuiPlatform::endFrame() {
|
||||
ImDrawData* drawData = ImGui::GetDrawData();
|
||||
if (!drawData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const halnf margin = mWindow.frame().margin();
|
||||
drawData->DisplayPos = ImVec2(-margin, -margin);
|
||||
drawData->DisplaySize = ImVec2(mWindow.frame().surfaceSize().x, mWindow.frame().surfaceSize().y);
|
||||
|
||||
const bool wantsText = ImGui::GetIO().WantTextInput;
|
||||
if (wantsText != mTextInputWanted) {
|
||||
mTextInputWanted = wantsText;
|
||||
mWindow.services().setTextInputEnabled(wantsText);
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiPlatform::consumeInput(const InputState& input) {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
for (const InputEvent& event : input.events()) {
|
||||
std::visit(
|
||||
InputEventVisitor{
|
||||
[&io](const PointerMoved& moved) { io.AddMousePosEvent(moved.position.x, moved.position.y); },
|
||||
[&io](const PointerLeft&) { io.AddMousePosEvent(-FLT_MAX, -FLT_MAX); },
|
||||
[&io](const PointerButtonChanged& changed) {
|
||||
io.AddMouseButtonEvent(imguiMouseButton(changed.button), changed.pressed);
|
||||
},
|
||||
[&io](const Scrolled& scrolled) { io.AddMouseWheelEvent(scrolled.delta.x, scrolled.delta.y); },
|
||||
[this](const KeyChanged& changed) { onKey(changed.key, changed.action != KeyAction::Release); },
|
||||
[&io](const TextEntered& text) { io.AddInputCharactersUTF8(text.utf8.c_str()); },
|
||||
[&io](const KeyboardFocusChanged& focus) { io.AddFocusEvent(focus.focused); },
|
||||
[](const PointerSampled&) {},
|
||||
[](const ScrollEnded&) {},
|
||||
[](const GestureChanged&) {},
|
||||
},
|
||||
event
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiPlatform::onKey(Key key, bool pressed) {
|
||||
updateModifier(key, pressed);
|
||||
|
||||
const ImGuiKey imgui = imguiKey(key);
|
||||
if (imgui != ImGuiKey_None) {
|
||||
ImGui::GetIO().AddKeyEvent(imgui, pressed);
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiPlatform::updateModifier(Key key, bool pressed) {
|
||||
const ImGuiKey modifier = imguiModifier(key);
|
||||
if (modifier == ImGuiKey_None) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t side = isRightModifier(key) ? 1 : 0;
|
||||
mModifierSides[(size_t) modifierIndex(modifier)][side] = pressed;
|
||||
|
||||
const bool down =
|
||||
mModifierSides[(size_t) modifierIndex(modifier)][0] || mModifierSides[(size_t) modifierIndex(modifier)][1];
|
||||
ImGui::GetIO().AddKeyEvent(modifier, down);
|
||||
}
|
||||
|
||||
void ImGuiPlatform::updateCursor() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
if (io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange) {
|
||||
return;
|
||||
}
|
||||
if (!io.WantCaptureMouse) {
|
||||
mCursor = -2;
|
||||
return;
|
||||
}
|
||||
|
||||
const ImGuiMouseCursor cursor = ImGui::GetMouseCursor();
|
||||
if (cursor == mCursor) {
|
||||
return;
|
||||
}
|
||||
|
||||
mCursor = cursor;
|
||||
mWindow.services().setCursor(cursorShape(cursor));
|
||||
}
|
||||
|
||||
const char* ImGuiPlatform::readClipboard(void* userData) {
|
||||
auto* self = static_cast<ImGuiPlatform*>(userData);
|
||||
self->mClipboard = self->mWindow.services().clipboardText();
|
||||
return self->mClipboard.c_str();
|
||||
}
|
||||
|
||||
void ImGuiPlatform::writeClipboard(void* userData, const char* text) {
|
||||
static_cast<ImGuiPlatform*>(userData)->mWindow.services().setClipboardText(text ? text : "");
|
||||
}
|
||||
|
||||
void ImGuiPlatform::placeInputMethod(ImGuiViewport*, ImGuiPlatformImeData* data) {
|
||||
auto* self = static_cast<ImGuiPlatform*>(ImGui::GetIO().BackendPlatformUserData);
|
||||
if (!self || !data->WantVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
self->mWindow.services().setTextInputRect({ data->InputPos.x, data->InputPos.y, 1, data->InputLineHeight });
|
||||
}
|
||||
43
GraphicApplication/src/debug_ui/private/ImGuiPlatform.hpp
Normal file
43
GraphicApplication/src/debug_ui/private/ImGuiPlatform.hpp
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputState.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
struct ImGuiViewport;
|
||||
struct ImGuiPlatformImeData;
|
||||
|
||||
namespace tp {
|
||||
|
||||
class Window;
|
||||
|
||||
class ImGuiPlatform {
|
||||
public:
|
||||
explicit ImGuiPlatform(Window& window);
|
||||
~ImGuiPlatform();
|
||||
|
||||
ImGuiPlatform(const ImGuiPlatform&) = delete;
|
||||
ImGuiPlatform& operator=(const ImGuiPlatform&) = delete;
|
||||
|
||||
void newFrame();
|
||||
void endFrame();
|
||||
|
||||
private:
|
||||
void consumeInput(const InputState& input);
|
||||
void onKey(Key key, bool pressed);
|
||||
|
||||
static const char* readClipboard(void* userData);
|
||||
static void writeClipboard(void* userData, const char* text);
|
||||
static void placeInputMethod(ImGuiViewport* viewport, ImGuiPlatformImeData* data);
|
||||
|
||||
void updateCursor();
|
||||
void updateModifier(Key key, bool pressed);
|
||||
|
||||
Window& mWindow;
|
||||
std::string mClipboard;
|
||||
double mPreviousTime = 0;
|
||||
int mCursor = -2;
|
||||
bool mModifierSides[4][2] = {};
|
||||
bool mTextInputWanted = false;
|
||||
};
|
||||
}
|
||||
35
GraphicApplication/src/debug_ui/public/DebugGUI.hpp
Normal file
35
GraphicApplication/src/debug_ui/public/DebugGUI.hpp
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#pragma once
|
||||
|
||||
#include "Window.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
namespace tp {
|
||||
|
||||
// Visible renders ImGui into the window and feeds it input; Headless keeps the ImGui context and frames alive
|
||||
// for code that calls ImGui directly, without drawing or taking input.
|
||||
enum class DebugInterface { Visible, Headless };
|
||||
|
||||
// Owns the process ImGui and ImPlot contexts. ImGui calls are valid between drawBegin and drawEnd.
|
||||
class DebugGUI {
|
||||
class Context;
|
||||
|
||||
public:
|
||||
DebugGUI(Window& window, const std::filesystem::path& resourceDirectory, DebugInterface mode);
|
||||
~DebugGUI();
|
||||
|
||||
DebugGUI(const DebugGUI&) = delete;
|
||||
DebugGUI& operator=(const DebugGUI&) = delete;
|
||||
|
||||
[[nodiscard]] DebugInterface mode() const;
|
||||
|
||||
void drawBegin();
|
||||
void drawEnd(const GpuFrame& frame);
|
||||
|
||||
void showValue(const char* label, halnf value);
|
||||
|
||||
private:
|
||||
std::unique_ptr<Context> mContext;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
#include "WindowDecorations.hpp"
|
||||
|
||||
using namespace tp;
|
||||
|
||||
namespace {
|
||||
const RGBA kTransparent = { 0, 0, 0, 0 };
|
||||
const RGBA kOpaque = { 0, 0, 0, 1 };
|
||||
|
||||
RectF contentArea(const WindowFrame& frame) { return { { 0, 0 }, frame.contentSize() }; }
|
||||
}
|
||||
|
||||
void WindowDecorations::paintBase(Canvas& canvas, const WindowFrame& frame) const {
|
||||
canvas.rect(contentArea(frame), mStyle.base);
|
||||
}
|
||||
|
||||
void WindowDecorations::paintFrame(Canvas& canvas, const WindowFrame& frame, bool focused) const {
|
||||
if (!frame.hasShadow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Canvas::SavedState saved(canvas);
|
||||
cutCorners(canvas, frame);
|
||||
paintOutline(canvas, frame);
|
||||
paintShadow(canvas, frame, focused);
|
||||
}
|
||||
|
||||
void WindowDecorations::cutCorners(Canvas& canvas, const WindowFrame& frame) const {
|
||||
canvas.setCompositeOperation(Canvas::CompositeOperation::DestinationOut);
|
||||
canvas.beginPath();
|
||||
canvas.addPathRect(contentArea(frame));
|
||||
canvas.addPathRect(contentArea(frame), frame.cornerRadius());
|
||||
canvas.setPathWinding(Canvas::PathWinding::Hole);
|
||||
canvas.fillPath(kOpaque);
|
||||
}
|
||||
|
||||
void WindowDecorations::paintOutline(Canvas& canvas, const WindowFrame& frame) const {
|
||||
canvas.setCompositeOperation(Canvas::CompositeOperation::SourceOver);
|
||||
canvas.outline(contentArea(frame), mStyle.outline, frame.cornerRadius(), mStyle.outlineWidth);
|
||||
}
|
||||
|
||||
void WindowDecorations::paintShadow(Canvas& canvas, const WindowFrame& frame, bool focused) const {
|
||||
const halnf margin = frame.margin();
|
||||
const Canvas::BoxGradient shadow = {
|
||||
.box = RectF(Vec2F(0.0f, mStyle.shadowOffsetY), frame.contentSize()),
|
||||
.radius = frame.cornerRadius(),
|
||||
.feather = margin * mStyle.shadowFeatherFraction,
|
||||
.innerColor = focused ? mStyle.focusedShadow : mStyle.unfocusedShadow,
|
||||
.outerColor = kTransparent,
|
||||
};
|
||||
|
||||
canvas.setCompositeOperation(Canvas::CompositeOperation::DestinationOver);
|
||||
canvas.beginPath();
|
||||
canvas.addPathRect({ { -margin, -margin }, frame.surfaceSize() });
|
||||
canvas.fillPath(shadow);
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
#include "Canvas.hpp"
|
||||
#include "WindowFrame.hpp"
|
||||
|
||||
namespace tp {
|
||||
|
||||
struct WindowDecorationStyle {
|
||||
RGBA base = { 0.118f, 0.122f, 0.133f, 1.0f };
|
||||
RGBA outline = { 1.0f, 1.0f, 1.0f, 0.07f };
|
||||
RGBA focusedShadow = { 0, 0, 0, 0.42f };
|
||||
RGBA unfocusedShadow = { 0, 0, 0, 0.24f };
|
||||
halnf shadowFeatherFraction = 0.9f;
|
||||
halnf shadowOffsetY = 3;
|
||||
halnf outlineWidth = 1;
|
||||
};
|
||||
|
||||
// Client-side decorations of a translucent desktop window: base, rounded corners, outline and drop shadow.
|
||||
// Geometry and edge hit-testing come from WindowFrame, which the backend uses for input; this paints the same frame.
|
||||
class WindowDecorations {
|
||||
public:
|
||||
WindowDecorations() = default;
|
||||
explicit WindowDecorations(const WindowDecorationStyle& style) :
|
||||
mStyle(style) {}
|
||||
|
||||
void paintBase(Canvas& canvas, const WindowFrame& frame) const;
|
||||
void paintFrame(Canvas& canvas, const WindowFrame& frame, bool focused) const;
|
||||
|
||||
private:
|
||||
void cutCorners(Canvas& canvas, const WindowFrame& frame) const;
|
||||
void paintOutline(Canvas& canvas, const WindowFrame& frame) const;
|
||||
void paintShadow(Canvas& canvas, const WindowFrame& frame, bool focused) const;
|
||||
|
||||
WindowDecorationStyle mStyle;
|
||||
};
|
||||
}
|
||||
393
GraphicApplication/src/diagnostics/private/HangWatchdog.cpp
Normal file
393
GraphicApplication/src/diagnostics/private/HangWatchdog.cpp
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
#include "HangWatchdog.hpp"
|
||||
|
||||
#include "StackTrace.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <sys/prctl.h>
|
||||
#endif
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
int64_t nowNs() {
|
||||
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
std::string timestamp(const char* pattern) {
|
||||
const std::time_t now = std::time(nullptr);
|
||||
char text[64];
|
||||
std::strftime(text, sizeof(text), pattern, std::localtime(&now));
|
||||
return text;
|
||||
}
|
||||
|
||||
void writeFile(const std::filesystem::path& path, const std::string& text, std::ios::openmode mode) {
|
||||
std::ofstream file(path, mode);
|
||||
file << text;
|
||||
}
|
||||
|
||||
constexpr double kCheckIntervalMs = 100;
|
||||
}
|
||||
|
||||
HangWatchdog::HangWatchdog(HangSettings settings, UiTrail& trail) :
|
||||
mSettings(std::move(settings)),
|
||||
mTrail(trail) {
|
||||
mSettings.directory = directoryFor(mSettings.directory.empty() ? defaultHangsDirectory() : mSettings.directory);
|
||||
if (!mSettings.clockNs) {
|
||||
mSettings.clockNs = nowNs;
|
||||
}
|
||||
mPhaseStartNs.store(now());
|
||||
allowDebuggerAttach();
|
||||
StackTrace::prepareThreadSampling();
|
||||
mUiTid = StackTrace::callingThreadId();
|
||||
mTrail.trackCallingThread();
|
||||
pruneOldReports();
|
||||
mThread = std::thread([this]() { watch(); });
|
||||
}
|
||||
|
||||
HangWatchdog::~HangWatchdog() {
|
||||
{
|
||||
std::lock_guard lock(mStopMutex);
|
||||
mStopping = true;
|
||||
}
|
||||
mStopped.notify_all();
|
||||
mThread.join();
|
||||
mTrail.stopTracking();
|
||||
}
|
||||
|
||||
std::string HangWatchdog::formatThreads(const std::vector<ThreadSample>& threads, int uiTid) {
|
||||
std::ostringstream text;
|
||||
for (const ThreadSample& thread : threads) {
|
||||
text << (thread.tid == uiTid ? "UI THREAD " : "thread ") << thread.tid << " \"" << thread.name << "\" state "
|
||||
<< thread.state;
|
||||
if (!thread.waitChannel.empty() && thread.waitChannel != "0") {
|
||||
text << " waiting in " << thread.waitChannel;
|
||||
}
|
||||
text << "\n";
|
||||
if (!thread.answered) {
|
||||
text << " (did not answer the sample signal)\n";
|
||||
}
|
||||
for (uintptr_t frame : thread.frames) {
|
||||
text << " " << StackTrace::describe(frame) << "\n";
|
||||
}
|
||||
text << "\n";
|
||||
}
|
||||
return text.str();
|
||||
}
|
||||
|
||||
void HangWatchdog::enterPhase(UiPhase phase) {
|
||||
mPhaseStartNs.store(now(), std::memory_order_relaxed);
|
||||
mPhaseInputEvents.store(mInputEvents.load(std::memory_order_relaxed), std::memory_order_relaxed);
|
||||
mPhase.store(phase, std::memory_order_relaxed);
|
||||
mBeat.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
void HangWatchdog::noteInputEvents(uint64_t count) { mInputEvents.fetch_add(count, std::memory_order_relaxed); }
|
||||
|
||||
int64_t HangWatchdog::now() const { return mSettings.clockNs(); }
|
||||
|
||||
void HangWatchdog::addReportFileSource(ReportFileSource reportFileSource) {
|
||||
std::lock_guard lock(mReportFileSourcesMutex);
|
||||
mReportFileSources.push_back(std::move(reportFileSource));
|
||||
}
|
||||
|
||||
std::filesystem::path HangWatchdog::defaultHangsDirectory() {
|
||||
const char* home = std::getenv("HOME");
|
||||
const std::filesystem::path root = home && *home ? std::filesystem::path(home) : std::filesystem::current_path();
|
||||
return root / processName() / "hangs";
|
||||
}
|
||||
|
||||
std::filesystem::path HangWatchdog::directoryFor(const std::filesystem::path& applicationHangsDirectory) {
|
||||
if (const char* configured = std::getenv("MODULES_HANG_DIR"); configured && *configured) {
|
||||
return configured;
|
||||
}
|
||||
return applicationHangsDirectory;
|
||||
}
|
||||
|
||||
std::string HangWatchdog::processName() {
|
||||
#if defined(__linux__)
|
||||
std::ifstream comm("/proc/self/comm");
|
||||
std::string name;
|
||||
if (std::getline(comm, name) && !name.empty()) {
|
||||
return name;
|
||||
}
|
||||
#endif
|
||||
return "app";
|
||||
}
|
||||
|
||||
void HangWatchdog::allowDebuggerAttach() {
|
||||
#if defined(__linux__) && defined(PR_SET_PTRACER)
|
||||
prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
const char* HangWatchdog::phaseName(UiPhase phase) {
|
||||
switch (phase) {
|
||||
case UiPhase::Idle: return "idle";
|
||||
case UiPhase::Input: return "processing input";
|
||||
case UiPhase::Draw: return "drawing";
|
||||
case UiPhase::Present: return "presenting";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
void HangWatchdog::watch() {
|
||||
Stall stall;
|
||||
int64_t previous = now();
|
||||
std::unique_lock lock(mStopMutex);
|
||||
while (!mStopping) {
|
||||
mStopped.wait_for(lock, std::chrono::duration<double, std::milli>(kCheckIntervalMs), [this]() {
|
||||
return mStopping;
|
||||
});
|
||||
if (mStopping) {
|
||||
return;
|
||||
}
|
||||
lock.unlock();
|
||||
const int64_t current = now();
|
||||
discountProcessPause(current, previous);
|
||||
inspect(stall, current);
|
||||
previous = current;
|
||||
mInspections.fetch_add(1);
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
|
||||
void HangWatchdog::discountProcessPause(int64_t now, int64_t previous) {
|
||||
const double sinceLastCheckMs = (double) (now - previous) / 1.0e6;
|
||||
const double pausedMs = sinceLastCheckMs - kCheckIntervalMs;
|
||||
if (pausedMs < mSettings.processPauseMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t beat = mBeat.load(std::memory_order_acquire);
|
||||
if (beat != mPausedBeat) {
|
||||
mPausedBeat = beat;
|
||||
mPausedNs = 0;
|
||||
}
|
||||
mPausedNs += (int64_t) (pausedMs * 1.0e6);
|
||||
}
|
||||
|
||||
double HangWatchdog::stalledMs(int64_t now) const {
|
||||
const bool pausedThisPhase = mPausedBeat == mBeat.load(std::memory_order_acquire);
|
||||
const int64_t paused = pausedThisPhase ? mPausedNs : 0;
|
||||
return (double) (now - mPhaseStartNs.load(std::memory_order_relaxed) - paused) / 1.0e6;
|
||||
}
|
||||
|
||||
uint64_t HangWatchdog::inputEventsInPhase() const {
|
||||
return mInputEvents.load(std::memory_order_relaxed) - mPhaseInputEvents.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void HangWatchdog::inspect(Stall& stall, int64_t now) {
|
||||
const uint64_t beat = mBeat.load(std::memory_order_acquire);
|
||||
const UiPhase phase = mPhase.load(std::memory_order_relaxed);
|
||||
const double stalled = stalledMs(now);
|
||||
|
||||
if (!stall.report.empty() && stall.beat != beat) {
|
||||
finishRecovered(stall, stalled);
|
||||
stall = {};
|
||||
}
|
||||
if (phase == UiPhase::Idle || stalled < mSettings.thresholdMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stall.report.empty()) {
|
||||
stall.beat = beat;
|
||||
stall.phase = phase;
|
||||
beginReport(stall, stalled);
|
||||
return;
|
||||
}
|
||||
if (stall.followUps < mSettings.followUpMs.size() && stalled >= mSettings.followUpMs[stall.followUps]) {
|
||||
appendFollowUp(stall, stalled);
|
||||
}
|
||||
}
|
||||
|
||||
std::string HangWatchdog::uiSnapshot(double stalled, UiPhase phase) const {
|
||||
std::ostringstream text;
|
||||
text << "ui thread " << phaseName(phase) << " for " << (int64_t) stalled << " ms\n";
|
||||
text << "input events handled during this phase " << inputEventsInPhase() << "\n";
|
||||
text << "scope trail (outermost first):\n";
|
||||
const std::vector<std::string> names = mTrail.snapshot();
|
||||
if (names.empty()) {
|
||||
text << " (no profiled scope open)\n";
|
||||
}
|
||||
for (size_t index = 0; index < names.size(); index++) {
|
||||
text << " " << std::string(index * 2, ' ') << names[index] << "\n";
|
||||
}
|
||||
return text.str();
|
||||
}
|
||||
|
||||
std::filesystem::path HangWatchdog::newReportPath() const {
|
||||
const std::string base = mSettings.application + "-" + timestamp("%Y-%m-%d_%H-%M-%S") + "-build" + mSettings.build;
|
||||
std::filesystem::path path = mSettings.directory / base;
|
||||
for (int suffix = 2; std::filesystem::exists(path); suffix++) {
|
||||
path = mSettings.directory / (base + "-" + std::to_string(suffix));
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
std::string HangWatchdog::reportHeader() const {
|
||||
std::ostringstream text;
|
||||
text << "application " << mSettings.application << "\n";
|
||||
text << "build " << mSettings.build << "\n";
|
||||
text << "detected " << timestamp("%Y-%m-%d %H:%M:%S") << "\n";
|
||||
text << "threshold " << (int64_t) mSettings.thresholdMs << " ms\n";
|
||||
text << "debugger attached " << (StackTrace::isDebuggerAttached() ? "yes" : "no") << "\n\n";
|
||||
return text.str();
|
||||
}
|
||||
|
||||
void HangWatchdog::beginReport(Stall& stall, double stalled) {
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(mSettings.directory, error);
|
||||
stall.report = newReportPath();
|
||||
std::filesystem::create_directories(stall.report, error);
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
writeFile(
|
||||
stall.report / "summary.txt", reportHeader() + uiSnapshot(stalled, stall.phase), std::ios::out | std::ios::trunc
|
||||
);
|
||||
writeSourcedFiles(stall.report);
|
||||
mReportsWritten.fetch_add(1);
|
||||
|
||||
const Investigation investigation = investigate();
|
||||
writeFile(
|
||||
stall.report / "summary.txt", "\nverdict: " + investigation.verdict + "\n", std::ios::out | std::ios::app
|
||||
);
|
||||
if (!investigation.threads.empty()) {
|
||||
writeFile(stall.report / "threads.txt", investigation.threads, std::ios::out | std::ios::trunc);
|
||||
}
|
||||
pruneOldReports();
|
||||
}
|
||||
|
||||
void HangWatchdog::writeSourcedFiles(const std::filesystem::path& report) {
|
||||
std::vector<ReportFileSource> reportFileSources;
|
||||
{
|
||||
std::lock_guard lock(mReportFileSourcesMutex);
|
||||
reportFileSources = mReportFileSources;
|
||||
}
|
||||
for (const ReportFileSource& reportFileSource : reportFileSources) {
|
||||
for (const HangReportFile& file : reportFileSource()) {
|
||||
writeFile(report / file.fileName, file.text, std::ios::out | std::ios::trunc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HangWatchdog::appendFollowUp(Stall& stall, double stalled) {
|
||||
stall.followUps++;
|
||||
const std::string header = "\nstill stuck at " + timestamp("%H:%M:%S") + "\n";
|
||||
writeFile(stall.report / "summary.txt", header + uiSnapshot(stalled, stall.phase), std::ios::out | std::ios::app);
|
||||
|
||||
const Investigation investigation = investigate();
|
||||
writeFile(stall.report / "summary.txt", "verdict: " + investigation.verdict + "\n", std::ios::out | std::ios::app);
|
||||
if (!investigation.threads.empty()) {
|
||||
writeFile(
|
||||
stall.report / "threads.txt",
|
||||
"\n===== follow-up at " + timestamp("%H:%M:%S") + " =====\n" + investigation.threads,
|
||||
std::ios::out | std::ios::app
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void HangWatchdog::finishRecovered(Stall& stall, double stalled) {
|
||||
(void) stalled;
|
||||
writeFile(
|
||||
stall.report / "summary.txt", "\nrecovered at " + timestamp("%H:%M:%S") + "\n", std::ios::out | std::ios::app
|
||||
);
|
||||
}
|
||||
|
||||
HangWatchdog::Investigation HangWatchdog::investigate() const {
|
||||
if (StackTrace::isDebuggerAttached()) {
|
||||
return { "not sampled: a debugger is attached, its thread view shows where the ui thread is", "" };
|
||||
}
|
||||
if (!StackTrace::canSampleThreads()) {
|
||||
return { "not sampled: thread stacks are not available on this platform", "" };
|
||||
}
|
||||
|
||||
std::vector<std::vector<uintptr_t>> uiSamples;
|
||||
std::string uiState;
|
||||
std::string uiWait;
|
||||
std::string threads = sampleStacks(uiSamples, uiState, uiWait);
|
||||
std::string verdict = stackVerdict(uiSamples, uiState, uiWait);
|
||||
if (inputEventsInPhase() > 0) {
|
||||
verdict +=
|
||||
"; " + std::to_string(inputEventsInPhase()) + " input events were handled without leaving the input phase";
|
||||
}
|
||||
return { verdict, threads };
|
||||
}
|
||||
|
||||
std::string HangWatchdog::sampleStacks(
|
||||
std::vector<std::vector<uintptr_t>>& uiSamples, std::string& uiState, std::string& uiWait
|
||||
) const {
|
||||
std::ostringstream text;
|
||||
for (int sample = 0; sample < std::max(1, mSettings.stackSamples); sample++) {
|
||||
if (sample > 0) {
|
||||
std::this_thread::sleep_for(std::chrono::duration<double, std::milli>(mSettings.stackSampleIntervalMs));
|
||||
}
|
||||
const std::vector<ThreadSample> threads = StackTrace::sampleAllThreads();
|
||||
for (const ThreadSample& thread : threads) {
|
||||
if (thread.tid == mUiTid) {
|
||||
uiSamples.push_back(thread.frames);
|
||||
uiState = thread.state;
|
||||
uiWait = thread.waitChannel;
|
||||
}
|
||||
}
|
||||
text << "===== sample " << sample + 1 << " =====\n" << formatThreads(threads, mUiTid);
|
||||
}
|
||||
return text.str();
|
||||
}
|
||||
|
||||
std::string HangWatchdog::stackVerdict(
|
||||
const std::vector<std::vector<uintptr_t>>& uiSamples, const std::string& state, const std::string& waitChannel
|
||||
) {
|
||||
if (uiSamples.empty()) {
|
||||
return "ui thread not found";
|
||||
}
|
||||
const bool sameStack =
|
||||
std::all_of(uiSamples.begin(), uiSamples.end(), [&](const auto& frames) { return frames == uiSamples.front(); });
|
||||
const bool sleeping = !state.empty() && (state[0] == 'S' || state[0] == 'D');
|
||||
if (sameStack && sleeping) {
|
||||
std::string text = "blocked: the ui thread sleeps at the same place in every sample";
|
||||
if (!waitChannel.empty() && waitChannel != "0") {
|
||||
text += " (kernel wait " + waitChannel + ")";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
if (!sleeping) {
|
||||
return "busy: the ui thread is running, check the changing frames in threads.txt";
|
||||
}
|
||||
return "waiting repeatedly: the ui thread sleeps but at different places, likely polling or lock contention";
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> HangWatchdog::reports() const {
|
||||
std::vector<std::filesystem::path> found;
|
||||
std::error_code error;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(mSettings.directory, error)) {
|
||||
if (entry.is_directory()) {
|
||||
found.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
std::sort(found.begin(), found.end(), [](const auto& a, const auto& b) {
|
||||
return std::filesystem::last_write_time(a) < std::filesystem::last_write_time(b);
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
void HangWatchdog::pruneOldReports() const {
|
||||
const std::vector<std::filesystem::path> found = reports();
|
||||
if (found.size() <= mSettings.keptReports) {
|
||||
return;
|
||||
}
|
||||
std::error_code error;
|
||||
for (size_t index = 0; index + mSettings.keptReports < found.size(); index++) {
|
||||
std::filesystem::remove_all(found[index], error);
|
||||
}
|
||||
}
|
||||
}
|
||||
219
GraphicApplication/src/diagnostics/private/Profiler.cpp
Normal file
219
GraphicApplication/src/diagnostics/private/Profiler.cpp
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
#include "Profiler.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <sys/resource.h>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
double clockMs(clockid_t clock) {
|
||||
timespec now{};
|
||||
clock_gettime(clock, &now);
|
||||
return (double) now.tv_sec * 1000.0 + (double) now.tv_nsec / 1.0e6;
|
||||
}
|
||||
|
||||
int64_t monotonicNs() {
|
||||
timespec now{};
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (int64_t) now.tv_sec * 1000000000ll + now.tv_nsec;
|
||||
}
|
||||
|
||||
double percentile(const std::vector<double>& sorted, double fraction) {
|
||||
if (sorted.empty()) {
|
||||
return 0;
|
||||
}
|
||||
const double position = fraction * (double) (sorted.size() - 1);
|
||||
const size_t lower = (size_t) std::floor(position);
|
||||
const size_t upper = std::min(lower + 1, sorted.size() - 1);
|
||||
const double blend = position - (double) lower;
|
||||
return sorted[lower] + (sorted[upper] - sorted[lower]) * blend;
|
||||
}
|
||||
}
|
||||
|
||||
ProfileStatistics summarize(std::vector<double> values) {
|
||||
ProfileStatistics statistics;
|
||||
statistics.samples = values.size();
|
||||
if (values.empty()) {
|
||||
return statistics;
|
||||
}
|
||||
|
||||
std::sort(values.begin(), values.end());
|
||||
for (double value : values) {
|
||||
statistics.total += value;
|
||||
}
|
||||
statistics.mean = statistics.total / (double) values.size();
|
||||
statistics.minimum = values.front();
|
||||
statistics.maximum = values.back();
|
||||
statistics.median = percentile(values, 0.5);
|
||||
statistics.p95 = percentile(values, 0.95);
|
||||
statistics.p99 = percentile(values, 0.99);
|
||||
return statistics;
|
||||
}
|
||||
|
||||
void Profiler::setEnabled(bool enabled) {
|
||||
mEnabled = enabled;
|
||||
if (!enabled) {
|
||||
mInFrame = false;
|
||||
}
|
||||
}
|
||||
|
||||
ProfileId Profiler::intern(std::deque<std::string>& names, const char* name) {
|
||||
const ptrdiff_t existing = indexOf(names, name);
|
||||
if (existing >= 0) {
|
||||
return (ProfileId) existing;
|
||||
}
|
||||
if (names.size() >= kMaxNames) {
|
||||
return (ProfileId) (kMaxNames - 1);
|
||||
}
|
||||
names.emplace_back(name);
|
||||
return (ProfileId) (names.size() - 1);
|
||||
}
|
||||
|
||||
ptrdiff_t Profiler::indexOf(const std::deque<std::string>& names, const std::string& name) {
|
||||
const auto found = std::find(names.begin(), names.end(), name);
|
||||
return found == names.end() ? -1 : found - names.begin();
|
||||
}
|
||||
|
||||
ProfileId Profiler::section(const char* name) { return intern(mSectionNames, name); }
|
||||
|
||||
ProfileId Profiler::counter(const char* name) { return intern(mCounterNames, name); }
|
||||
|
||||
const char* Profiler::sectionName(ProfileId section) const {
|
||||
return section < mSectionNames.size() ? mSectionNames[section].c_str() : "?";
|
||||
}
|
||||
|
||||
void Profiler::beginFrame() {
|
||||
if (!mEnabled) {
|
||||
return;
|
||||
}
|
||||
mSectionMs.fill(0);
|
||||
mSectionCalls.fill(0);
|
||||
mCounters.fill(0);
|
||||
mFrameWallStart = clockMs(CLOCK_MONOTONIC);
|
||||
mFrameCpuStart = threadCpuMs();
|
||||
mInFrame = true;
|
||||
}
|
||||
|
||||
void Profiler::endFrame() {
|
||||
if (!mEnabled || !mInFrame) {
|
||||
return;
|
||||
}
|
||||
mInFrame = false;
|
||||
|
||||
FrameProfile frame;
|
||||
frame.index = mNextFrame++;
|
||||
frame.wallMs = clockMs(CLOCK_MONOTONIC) - mFrameWallStart;
|
||||
frame.cpuMs = threadCpuMs() - mFrameCpuStart;
|
||||
frame.sectionMs.assign(mSectionMs.begin(), mSectionMs.begin() + (ptrdiff_t) mSectionNames.size());
|
||||
frame.sectionCalls.assign(mSectionCalls.begin(), mSectionCalls.begin() + (ptrdiff_t) mSectionNames.size());
|
||||
frame.counters.assign(mCounters.begin(), mCounters.begin() + (ptrdiff_t) mCounterNames.size());
|
||||
mFrames.push_back(std::move(frame));
|
||||
}
|
||||
|
||||
void Profiler::addSectionTime(ProfileId section, double milliseconds) {
|
||||
if (!mInFrame || section >= kMaxNames) {
|
||||
return;
|
||||
}
|
||||
mSectionMs[section] += milliseconds;
|
||||
mSectionCalls[section]++;
|
||||
}
|
||||
|
||||
void Profiler::addCounter(ProfileId counter, double amount) {
|
||||
if (!mInFrame || counter >= kMaxNames) {
|
||||
return;
|
||||
}
|
||||
mCounters[counter] += amount;
|
||||
}
|
||||
|
||||
void Profiler::clear() {
|
||||
mFrames.clear();
|
||||
mNextFrame = 0;
|
||||
mInFrame = false;
|
||||
}
|
||||
|
||||
std::vector<double> Profiler::sectionSeries(const std::string& name) const {
|
||||
std::vector<double> series;
|
||||
const ptrdiff_t index = indexOf(mSectionNames, name);
|
||||
if (index < 0) {
|
||||
return series;
|
||||
}
|
||||
for (const FrameProfile& frame : mFrames) {
|
||||
series.push_back((size_t) index < frame.sectionMs.size() ? frame.sectionMs[(size_t) index] : 0.0);
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
std::vector<double> Profiler::counterSeries(const std::string& name) const {
|
||||
std::vector<double> series;
|
||||
const ptrdiff_t index = indexOf(mCounterNames, name);
|
||||
if (index < 0) {
|
||||
return series;
|
||||
}
|
||||
for (const FrameProfile& frame : mFrames) {
|
||||
series.push_back((size_t) index < frame.counters.size() ? frame.counters[(size_t) index] : 0.0);
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
double Profiler::counterTotal(const std::string& name) const {
|
||||
double total = 0;
|
||||
for (double value : counterSeries(name)) {
|
||||
total += value;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
double Profiler::sectionTotal(const std::string& name) const {
|
||||
double total = 0;
|
||||
for (double value : sectionSeries(name)) {
|
||||
total += value;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
uint64_t Profiler::sectionCallTotal(const std::string& name) const {
|
||||
uint64_t total = 0;
|
||||
const ptrdiff_t index = indexOf(mSectionNames, name);
|
||||
if (index < 0) {
|
||||
return 0;
|
||||
}
|
||||
for (const FrameProfile& frame : mFrames) {
|
||||
if ((size_t) index < frame.sectionCalls.size()) {
|
||||
total += frame.sectionCalls[(size_t) index];
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
double Profiler::threadCpuMs() { return clockMs(CLOCK_THREAD_CPUTIME_ID); }
|
||||
|
||||
double Profiler::processCpuMs() { return clockMs(CLOCK_PROCESS_CPUTIME_ID); }
|
||||
|
||||
double Profiler::peakResidentMegabytes() {
|
||||
rusage usage{};
|
||||
getrusage(RUSAGE_SELF, &usage);
|
||||
return (double) usage.ru_maxrss / 1024.0;
|
||||
}
|
||||
|
||||
ProfileScope::ProfileScope(Profiler& profiler, ProfileId section) :
|
||||
mProfiler(profiler),
|
||||
mSection(section) {
|
||||
mProfiler.trail().push(mProfiler.sectionName(section));
|
||||
if (mProfiler.isInFrame()) {
|
||||
mStartNs = monotonicNs();
|
||||
}
|
||||
}
|
||||
|
||||
ProfileScope::ProfileScope(Profiler& profiler, const char* sectionName) :
|
||||
ProfileScope(profiler, profiler.section(sectionName)) {}
|
||||
|
||||
ProfileScope::~ProfileScope() {
|
||||
mProfiler.trail().pop();
|
||||
if (mStartNs < 0) {
|
||||
return;
|
||||
}
|
||||
mProfiler.addSectionTime(mSection, (double) (monotonicNs() - mStartNs) / 1.0e6);
|
||||
}
|
||||
}
|
||||
59
GraphicApplication/src/diagnostics/private/UiTrail.cpp
Normal file
59
GraphicApplication/src/diagnostics/private/UiTrail.cpp
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#include "UiTrail.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace tp {
|
||||
|
||||
void UiTrail::push(const char* name) {
|
||||
if (!isTracked()) {
|
||||
return;
|
||||
}
|
||||
const int depth = mDepth.load(std::memory_order_relaxed);
|
||||
if (depth < kDepth) {
|
||||
mNames[depth].store(name, std::memory_order_relaxed);
|
||||
} else {
|
||||
mOverflow.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
mDepth.store(depth + 1, std::memory_order_release);
|
||||
}
|
||||
|
||||
void UiTrail::pop() {
|
||||
if (!isTracked()) {
|
||||
return;
|
||||
}
|
||||
const int depth = mDepth.load(std::memory_order_relaxed);
|
||||
if (depth <= 0) {
|
||||
return;
|
||||
}
|
||||
if (depth > kDepth) {
|
||||
mOverflow.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
mDepth.store(depth - 1, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool UiTrail::isTracked() const {
|
||||
return mTrackedThread.load(std::memory_order_relaxed) == std::this_thread::get_id();
|
||||
}
|
||||
|
||||
void UiTrail::trackCallingThread() {
|
||||
mDepth.store(0);
|
||||
mOverflow.store(0);
|
||||
mTrackedThread.store(std::this_thread::get_id());
|
||||
}
|
||||
|
||||
void UiTrail::stopTracking() { mTrackedThread.store(std::thread::id{}); }
|
||||
|
||||
std::vector<std::string> UiTrail::snapshot() const {
|
||||
const int depth = std::min(mDepth.load(std::memory_order_acquire), kDepth);
|
||||
std::vector<std::string> names;
|
||||
for (int index = 0; index < depth; index++) {
|
||||
const char* name = mNames[index].load(std::memory_order_relaxed);
|
||||
names.emplace_back(name ? name : "?");
|
||||
}
|
||||
const int overflow = mOverflow.load(std::memory_order_relaxed);
|
||||
if (overflow > 0) {
|
||||
names.push_back("... " + std::to_string(overflow) + " deeper");
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
||||
122
GraphicApplication/src/diagnostics/public/HangWatchdog.hpp
Normal file
122
GraphicApplication/src/diagnostics/public/HangWatchdog.hpp
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
#pragma once
|
||||
|
||||
#include "StackTrace.hpp"
|
||||
#include "UiTrail.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
enum class UiPhase : uint8_t { Idle, Input, Draw, Present };
|
||||
|
||||
struct HangSettings {
|
||||
double thresholdMs = 2000;
|
||||
std::vector<double> followUpMs = { 10000, 30000 };
|
||||
int stackSamples = 5;
|
||||
double stackSampleIntervalMs = 200;
|
||||
double processPauseMs = 1000;
|
||||
size_t keptReports = 20;
|
||||
std::string application = "app";
|
||||
std::string build = "dev";
|
||||
std::filesystem::path directory;
|
||||
std::function<int64_t()> clockNs;
|
||||
};
|
||||
|
||||
struct HangReportFile {
|
||||
std::string fileName;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
class HangWatchdog {
|
||||
public:
|
||||
using ReportFileSource = std::function<std::vector<HangReportFile>()>;
|
||||
|
||||
HangWatchdog(HangSettings settings, UiTrail& trail);
|
||||
~HangWatchdog();
|
||||
|
||||
HangWatchdog(const HangWatchdog&) = delete;
|
||||
HangWatchdog& operator=(const HangWatchdog&) = delete;
|
||||
|
||||
void enterPhase(UiPhase phase);
|
||||
void noteInputEvents(uint64_t count = 1);
|
||||
void addReportFileSource(ReportFileSource reportFileSource);
|
||||
|
||||
[[nodiscard]] const std::filesystem::path& directory() const { return mSettings.directory; }
|
||||
[[nodiscard]] std::vector<std::filesystem::path> reports() const;
|
||||
[[nodiscard]] size_t reportsWritten() const { return mReportsWritten.load(); }
|
||||
[[nodiscard]] uint64_t inspections() const { return mInspections.load(); }
|
||||
|
||||
[[nodiscard]] static std::filesystem::path defaultHangsDirectory();
|
||||
[[nodiscard]] static std::filesystem::path directoryFor(const std::filesystem::path& applicationHangsDirectory);
|
||||
[[nodiscard]] static std::string processName();
|
||||
static void allowDebuggerAttach();
|
||||
[[nodiscard]] static const char* phaseName(UiPhase phase);
|
||||
|
||||
private:
|
||||
struct Stall {
|
||||
uint64_t beat = 0;
|
||||
UiPhase phase = UiPhase::Idle;
|
||||
std::filesystem::path report;
|
||||
size_t followUps = 0;
|
||||
};
|
||||
|
||||
struct Investigation {
|
||||
std::string verdict;
|
||||
std::string threads;
|
||||
};
|
||||
|
||||
void watch();
|
||||
void inspect(Stall& stall, int64_t now);
|
||||
void discountProcessPause(int64_t now, int64_t previous);
|
||||
[[nodiscard]] double stalledMs(int64_t now) const;
|
||||
[[nodiscard]] uint64_t inputEventsInPhase() const;
|
||||
|
||||
void beginReport(Stall& stall, double stalled);
|
||||
void appendFollowUp(Stall& stall, double stalled);
|
||||
void finishRecovered(Stall& stall, double stalled);
|
||||
[[nodiscard]] std::string reportHeader() const;
|
||||
[[nodiscard]] std::string uiSnapshot(double stalled, UiPhase phase) const;
|
||||
void writeSourcedFiles(const std::filesystem::path& report);
|
||||
[[nodiscard]] Investigation investigate() const;
|
||||
[[nodiscard]] std::string
|
||||
sampleStacks(std::vector<std::vector<uintptr_t>>& uiSamples, std::string& uiState, std::string& uiWait) const;
|
||||
[[nodiscard]] static std::string formatThreads(const std::vector<ThreadSample>& threads, int uiTid);
|
||||
[[nodiscard]] static std::string stackVerdict(
|
||||
const std::vector<std::vector<uintptr_t>>& uiSamples, const std::string& state, const std::string& waitChannel
|
||||
);
|
||||
|
||||
void pruneOldReports() const;
|
||||
[[nodiscard]] std::filesystem::path newReportPath() const;
|
||||
[[nodiscard]] int64_t now() const;
|
||||
|
||||
HangSettings mSettings;
|
||||
UiTrail& mTrail;
|
||||
std::atomic<uint64_t> mBeat{ 0 };
|
||||
std::atomic<int64_t> mPhaseStartNs{ 0 };
|
||||
std::atomic<UiPhase> mPhase{ UiPhase::Idle };
|
||||
std::atomic<uint64_t> mInputEvents{ 0 };
|
||||
std::atomic<uint64_t> mPhaseInputEvents{ 0 };
|
||||
std::atomic<size_t> mReportsWritten{ 0 };
|
||||
std::atomic<uint64_t> mInspections{ 0 };
|
||||
int mUiTid = 0;
|
||||
|
||||
uint64_t mPausedBeat = 0;
|
||||
int64_t mPausedNs = 0;
|
||||
|
||||
std::mutex mReportFileSourcesMutex;
|
||||
std::vector<ReportFileSource> mReportFileSources;
|
||||
|
||||
std::mutex mStopMutex;
|
||||
std::condition_variable mStopped;
|
||||
bool mStopping = false;
|
||||
std::thread mThread;
|
||||
};
|
||||
}
|
||||
116
GraphicApplication/src/diagnostics/public/Profiler.hpp
Normal file
116
GraphicApplication/src/diagnostics/public/Profiler.hpp
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#pragma once
|
||||
|
||||
#include "UiTrail.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
using ProfileId = uint32_t;
|
||||
|
||||
struct FrameProfile {
|
||||
uint64_t index = 0;
|
||||
double wallMs = 0;
|
||||
double cpuMs = 0;
|
||||
std::vector<double> sectionMs;
|
||||
std::vector<uint32_t> sectionCalls;
|
||||
std::vector<double> counters;
|
||||
};
|
||||
|
||||
struct ProfileStatistics {
|
||||
size_t samples = 0;
|
||||
double total = 0;
|
||||
double mean = 0;
|
||||
double minimum = 0;
|
||||
double median = 0;
|
||||
double p95 = 0;
|
||||
double p99 = 0;
|
||||
double maximum = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] ProfileStatistics summarize(std::vector<double> values);
|
||||
|
||||
// A frame holds two generic channels: sections measure milliseconds, counters
|
||||
// measure amounts. Nothing here knows which device or subsystem produced a
|
||||
// number - a GPU timer reports into the section channel like any other sampler.
|
||||
class Profiler {
|
||||
public:
|
||||
static constexpr size_t kMaxNames = 128;
|
||||
|
||||
void setEnabled(bool enabled);
|
||||
[[nodiscard]] bool isEnabled() const { return mEnabled; }
|
||||
|
||||
[[nodiscard]] ProfileId section(const char* name);
|
||||
[[nodiscard]] ProfileId counter(const char* name);
|
||||
|
||||
void beginFrame();
|
||||
void endFrame();
|
||||
[[nodiscard]] bool isInFrame() const { return mInFrame; }
|
||||
|
||||
// A duration measured outside the profiler (a GPU timer, a blocking wait, ...).
|
||||
// ProfileScope does the same for a scoped block. Both count one sample.
|
||||
void addSectionTime(ProfileId section, double milliseconds);
|
||||
void addCounter(ProfileId counter, double amount);
|
||||
|
||||
void clear();
|
||||
|
||||
[[nodiscard]] const std::vector<FrameProfile>& frames() const { return mFrames; }
|
||||
[[nodiscard]] const std::deque<std::string>& sectionNames() const { return mSectionNames; }
|
||||
[[nodiscard]] const std::deque<std::string>& counterNames() const { return mCounterNames; }
|
||||
[[nodiscard]] const char* sectionName(ProfileId section) const;
|
||||
|
||||
[[nodiscard]] std::vector<double> sectionSeries(const std::string& name) const;
|
||||
[[nodiscard]] std::vector<double> counterSeries(const std::string& name) const;
|
||||
[[nodiscard]] double counterTotal(const std::string& name) const;
|
||||
[[nodiscard]] double sectionTotal(const std::string& name) const;
|
||||
[[nodiscard]] uint64_t sectionCallTotal(const std::string& name) const;
|
||||
|
||||
// Open profile scopes of the frame loop, readable from a watchdog thread.
|
||||
[[nodiscard]] UiTrail& trail() { return mTrail; }
|
||||
[[nodiscard]] const UiTrail& trail() const { return mTrail; }
|
||||
|
||||
[[nodiscard]] static double threadCpuMs();
|
||||
[[nodiscard]] static double processCpuMs();
|
||||
[[nodiscard]] static double peakResidentMegabytes();
|
||||
|
||||
private:
|
||||
static ProfileId intern(std::deque<std::string>& names, const char* name);
|
||||
[[nodiscard]] static ptrdiff_t indexOf(const std::deque<std::string>& names, const std::string& name);
|
||||
|
||||
bool mEnabled = false;
|
||||
bool mInFrame = false;
|
||||
uint64_t mNextFrame = 0;
|
||||
double mFrameWallStart = 0;
|
||||
double mFrameCpuStart = 0;
|
||||
|
||||
std::deque<std::string> mSectionNames;
|
||||
std::deque<std::string> mCounterNames;
|
||||
|
||||
std::array<double, kMaxNames> mSectionMs{};
|
||||
std::array<uint32_t, kMaxNames> mSectionCalls{};
|
||||
std::array<double, kMaxNames> mCounters{};
|
||||
|
||||
std::vector<FrameProfile> mFrames;
|
||||
UiTrail mTrail;
|
||||
};
|
||||
|
||||
// Times a block into one section and records it on the trail while it is open.
|
||||
class ProfileScope {
|
||||
public:
|
||||
ProfileScope(Profiler& profiler, ProfileId section);
|
||||
ProfileScope(Profiler& profiler, const char* sectionName);
|
||||
~ProfileScope();
|
||||
|
||||
ProfileScope(const ProfileScope&) = delete;
|
||||
ProfileScope& operator=(const ProfileScope&) = delete;
|
||||
|
||||
private:
|
||||
Profiler& mProfiler;
|
||||
ProfileId mSection;
|
||||
int64_t mStartNs = -1;
|
||||
};
|
||||
}
|
||||
31
GraphicApplication/src/diagnostics/public/UiTrail.hpp
Normal file
31
GraphicApplication/src/diagnostics/public/UiTrail.hpp
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
// The stack of named scopes open on the tracked thread; another thread may take a snapshot at any time.
|
||||
class UiTrail {
|
||||
public:
|
||||
static constexpr int kDepth = 32;
|
||||
|
||||
void push(const char* name);
|
||||
void pop();
|
||||
|
||||
void trackCallingThread();
|
||||
void stopTracking();
|
||||
|
||||
[[nodiscard]] std::vector<std::string> snapshot() const;
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool isTracked() const;
|
||||
|
||||
std::atomic<const char*> mNames[kDepth] = {};
|
||||
std::atomic<int> mDepth{ 0 };
|
||||
std::atomic<int> mOverflow{ 0 };
|
||||
std::atomic<std::thread::id> mTrackedThread{};
|
||||
};
|
||||
}
|
||||
226
GraphicApplication/src/gpu/private/GpuDevice.cpp
Normal file
226
GraphicApplication/src/gpu/private/GpuDevice.cpp
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
#include "GpuDevice.hpp"
|
||||
|
||||
#include "GpuError.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
constexpr std::array<const char*, 1> kDeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
|
||||
constexpr const char* kValidationLayer = "VK_LAYER_KHRONOS_validation";
|
||||
|
||||
bool validationRequested() {
|
||||
const char* value = std::getenv("MODULES_VULKAN_VALIDATION");
|
||||
return value && std::strcmp(value, "1") == 0;
|
||||
}
|
||||
|
||||
bool hasValidationLayer() {
|
||||
uint32_t count = 0;
|
||||
vkEnumerateInstanceLayerProperties(&count, nullptr);
|
||||
std::vector<VkLayerProperties> layers(count);
|
||||
vkEnumerateInstanceLayerProperties(&count, layers.data());
|
||||
for (const VkLayerProperties& layer : layers) {
|
||||
if (std::strcmp(layer.layerName, kValidationLayer) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool supportsDeviceExtensions(VkPhysicalDevice device) {
|
||||
uint32_t count = 0;
|
||||
vkEnumerateDeviceExtensionProperties(device, nullptr, &count, nullptr);
|
||||
std::vector<VkExtensionProperties> available(count);
|
||||
vkEnumerateDeviceExtensionProperties(device, nullptr, &count, available.data());
|
||||
for (const char* required : kDeviceExtensions) {
|
||||
const bool found =
|
||||
std::any_of(available.begin(), available.end(), [required](const VkExtensionProperties& extension) {
|
||||
return std::strcmp(required, extension.extensionName) == 0;
|
||||
});
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t graphicsFamilyOf(VkPhysicalDevice device) {
|
||||
uint32_t count = 0;
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(device, &count, nullptr);
|
||||
std::vector<VkQueueFamilyProperties> families(count);
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(device, &count, families.data());
|
||||
for (uint32_t index = 0; index < count; index++) {
|
||||
if (families[index].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return UINT32_MAX;
|
||||
}
|
||||
}
|
||||
|
||||
GpuDevice::GpuDevice(const std::vector<const char*>& surfaceExtensions) {
|
||||
createInstance(surfaceExtensions);
|
||||
selectPhysicalDevice();
|
||||
createLogicalDevice();
|
||||
createCommandPool();
|
||||
}
|
||||
|
||||
GpuDevice::~GpuDevice() {
|
||||
waitIdle();
|
||||
if (mCommandPool != VK_NULL_HANDLE) {
|
||||
vkDestroyCommandPool(mDevice, mCommandPool, nullptr);
|
||||
}
|
||||
if (mDevice != VK_NULL_HANDLE) {
|
||||
vkDestroyDevice(mDevice, nullptr);
|
||||
}
|
||||
if (mInstance != VK_NULL_HANDLE) {
|
||||
vkDestroyInstance(mInstance, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void GpuDevice::createInstance(const std::vector<const char*>& surfaceExtensions) {
|
||||
std::vector<const char*> layers;
|
||||
if (validationRequested() && hasValidationLayer()) {
|
||||
layers.push_back(kValidationLayer);
|
||||
}
|
||||
|
||||
const VkApplicationInfo appInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
|
||||
.pApplicationName = "Modules",
|
||||
.apiVersion = VK_API_VERSION_1_2,
|
||||
};
|
||||
|
||||
const VkInstanceCreateInfo createInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
|
||||
.pApplicationInfo = &appInfo,
|
||||
.enabledLayerCount = (uint32_t) layers.size(),
|
||||
.ppEnabledLayerNames = layers.data(),
|
||||
.enabledExtensionCount = (uint32_t) surfaceExtensions.size(),
|
||||
.ppEnabledExtensionNames = surfaceExtensions.data(),
|
||||
};
|
||||
requireSuccess(vkCreateInstance(&createInfo, nullptr, &mInstance), "vkCreateInstance");
|
||||
}
|
||||
|
||||
void GpuDevice::selectPhysicalDevice() {
|
||||
uint32_t count = 0;
|
||||
vkEnumeratePhysicalDevices(mInstance, &count, nullptr);
|
||||
std::vector<VkPhysicalDevice> devices(count);
|
||||
vkEnumeratePhysicalDevices(mInstance, &count, devices.data());
|
||||
|
||||
for (VkPhysicalDevice device : devices) {
|
||||
const uint32_t graphics = graphicsFamilyOf(device);
|
||||
if (graphics == UINT32_MAX || !supportsDeviceExtensions(device)) {
|
||||
continue;
|
||||
}
|
||||
mPhysicalDevice = device;
|
||||
mGraphicsQueueFamily = graphics;
|
||||
vkGetPhysicalDeviceProperties(device, &mProperties);
|
||||
return;
|
||||
}
|
||||
throw GpuError("selecting a graphics device with swapchain support", VK_ERROR_INITIALIZATION_FAILED);
|
||||
}
|
||||
|
||||
void GpuDevice::createLogicalDevice() {
|
||||
const float priority = 1.0f;
|
||||
const VkDeviceQueueCreateInfo queueInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
|
||||
.queueFamilyIndex = mGraphicsQueueFamily,
|
||||
.queueCount = 1,
|
||||
.pQueuePriorities = &priority,
|
||||
};
|
||||
|
||||
VkPhysicalDeviceFeatures supported{};
|
||||
vkGetPhysicalDeviceFeatures(mPhysicalDevice, &supported);
|
||||
VkPhysicalDeviceFeatures features{};
|
||||
features.geometryShader = supported.geometryShader;
|
||||
|
||||
const VkDeviceCreateInfo createInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
|
||||
.queueCreateInfoCount = 1,
|
||||
.pQueueCreateInfos = &queueInfo,
|
||||
.enabledExtensionCount = (uint32_t) kDeviceExtensions.size(),
|
||||
.ppEnabledExtensionNames = kDeviceExtensions.data(),
|
||||
.pEnabledFeatures = &features,
|
||||
};
|
||||
requireSuccess(vkCreateDevice(mPhysicalDevice, &createInfo, nullptr, &mDevice), "vkCreateDevice");
|
||||
vkGetDeviceQueue(mDevice, mGraphicsQueueFamily, 0, &mGraphicsQueue);
|
||||
}
|
||||
|
||||
void GpuDevice::createCommandPool() {
|
||||
const VkCommandPoolCreateInfo poolInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
|
||||
.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
|
||||
.queueFamilyIndex = mGraphicsQueueFamily,
|
||||
};
|
||||
requireSuccess(vkCreateCommandPool(mDevice, &poolInfo, nullptr, &mCommandPool), "vkCreateCommandPool");
|
||||
}
|
||||
|
||||
bool GpuDevice::canPresent(VkSurfaceKHR surface) const {
|
||||
VkBool32 supported = VK_FALSE;
|
||||
vkGetPhysicalDeviceSurfaceSupportKHR(mPhysicalDevice, mGraphicsQueueFamily, surface, &supported);
|
||||
return supported == VK_TRUE;
|
||||
}
|
||||
|
||||
uint32_t GpuDevice::findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) const {
|
||||
VkPhysicalDeviceMemoryProperties memory{};
|
||||
vkGetPhysicalDeviceMemoryProperties(mPhysicalDevice, &memory);
|
||||
for (uint32_t index = 0; index < memory.memoryTypeCount; index++) {
|
||||
const bool allowed = typeFilter & (1u << index);
|
||||
if (allowed && (memory.memoryTypes[index].propertyFlags & properties) == properties) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
throw GpuError("finding a memory type", VK_ERROR_FEATURE_NOT_PRESENT);
|
||||
}
|
||||
|
||||
VkCommandBuffer GpuDevice::beginOneShotCommands() const {
|
||||
const VkCommandBufferAllocateInfo allocateInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
|
||||
.commandPool = mCommandPool,
|
||||
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
|
||||
.commandBufferCount = 1,
|
||||
};
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
requireSuccess(vkAllocateCommandBuffers(mDevice, &allocateInfo, &commandBuffer), "vkAllocateCommandBuffers");
|
||||
|
||||
const VkCommandBufferBeginInfo beginInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
|
||||
};
|
||||
requireSuccess(vkBeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer");
|
||||
return commandBuffer;
|
||||
}
|
||||
|
||||
// Waits on a dedicated fence rather than the whole queue, so in-flight window frames keep running.
|
||||
void GpuDevice::endOneShotCommands(VkCommandBuffer commandBuffer) const {
|
||||
requireSuccess(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer");
|
||||
|
||||
const VkFenceCreateInfo fenceInfo{ .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
requireSuccess(vkCreateFence(mDevice, &fenceInfo, nullptr, &fence), "vkCreateFence");
|
||||
|
||||
const VkSubmitInfo submitInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
|
||||
.commandBufferCount = 1,
|
||||
.pCommandBuffers = &commandBuffer,
|
||||
};
|
||||
const VkResult submitted = vkQueueSubmit(mGraphicsQueue, 1, &submitInfo, fence);
|
||||
if (submitted == VK_SUCCESS) {
|
||||
vkWaitForFences(mDevice, 1, &fence, VK_TRUE, UINT64_MAX);
|
||||
}
|
||||
|
||||
vkDestroyFence(mDevice, fence, nullptr);
|
||||
vkFreeCommandBuffers(mDevice, mCommandPool, 1, &commandBuffer);
|
||||
requireSuccess(submitted, "vkQueueSubmit");
|
||||
}
|
||||
|
||||
void GpuDevice::waitIdle() const {
|
||||
if (mDevice != VK_NULL_HANDLE) {
|
||||
vkDeviceWaitIdle(mDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
96
GraphicApplication/src/gpu/private/GpuTimer.cpp
Normal file
96
GraphicApplication/src/gpu/private/GpuTimer.cpp
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
#include "GpuTimer.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
GpuTimer::GpuTimer(
|
||||
VkDevice device, VkPhysicalDevice physicalDevice, uint32_t queueFamily, Profiler& profiler, const char* name
|
||||
) :
|
||||
mDevice(device),
|
||||
mProfiler(profiler),
|
||||
mSection(profiler.section(name)),
|
||||
mPending(kSlots, false) {
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
vkGetPhysicalDeviceProperties(physicalDevice, &properties);
|
||||
|
||||
uint32_t familyCount = 0;
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &familyCount, nullptr);
|
||||
std::vector<VkQueueFamilyProperties> families(familyCount);
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &familyCount, families.data());
|
||||
|
||||
const uint32_t validBits = queueFamily < familyCount ? families[queueFamily].timestampValidBits : 0;
|
||||
if (validBits == 0 || properties.limits.timestampPeriod <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
mTickNs = properties.limits.timestampPeriod;
|
||||
mValidMask = validBits >= 64 ? ~0ull : ((1ull << validBits) - 1ull);
|
||||
|
||||
VkQueryPoolCreateInfo info{
|
||||
.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO,
|
||||
.queryType = VK_QUERY_TYPE_TIMESTAMP,
|
||||
.queryCount = kSlots * 2,
|
||||
};
|
||||
if (vkCreateQueryPool(mDevice, &info, nullptr, &mPool) != VK_SUCCESS) {
|
||||
mPool = VK_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
|
||||
GpuTimer::~GpuTimer() {
|
||||
if (mPool != VK_NULL_HANDLE) {
|
||||
vkDestroyQueryPool(mDevice, mPool, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void GpuTimer::begin(VkCommandBuffer commandBuffer) {
|
||||
mOpenSlot = UINT32_MAX;
|
||||
if (!isSupported() || !mProfiler.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
collect();
|
||||
const uint32_t slot = mNextSlot;
|
||||
if (mPending[slot]) {
|
||||
return;
|
||||
}
|
||||
|
||||
vkCmdResetQueryPool(commandBuffer, mPool, slot * 2, 2);
|
||||
vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, mPool, slot * 2);
|
||||
mOpenSlot = slot;
|
||||
}
|
||||
|
||||
void GpuTimer::end(VkCommandBuffer commandBuffer) {
|
||||
if (mOpenSlot == UINT32_MAX) {
|
||||
return;
|
||||
}
|
||||
vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, mPool, mOpenSlot * 2 + 1);
|
||||
mPending[mOpenSlot] = true;
|
||||
mNextSlot = (mOpenSlot + 1) % kSlots;
|
||||
mOpenSlot = UINT32_MAX;
|
||||
}
|
||||
|
||||
void GpuTimer::collect() {
|
||||
if (!isSupported()) {
|
||||
return;
|
||||
}
|
||||
for (uint32_t offset = 0; offset < kSlots; offset++) {
|
||||
const uint32_t slot = (mNextSlot + offset) % kSlots;
|
||||
if (!mPending[slot]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint64_t stamps[2] = {};
|
||||
const VkResult result = vkGetQueryPoolResults(
|
||||
mDevice, mPool, slot * 2, 2, sizeof(stamps), stamps, sizeof(uint64_t), VK_QUERY_RESULT_64_BIT
|
||||
);
|
||||
if (result != VK_SUCCESS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
mPending[slot] = false;
|
||||
const uint64_t ticks = (stamps[1] - stamps[0]) & mValidMask;
|
||||
mProfiler.addSectionTime(mSection, (double) ticks * mTickNs / 1.0e6);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
GraphicApplication/src/gpu/private/GpuTimer.hpp
Normal file
43
GraphicApplication/src/gpu/private/GpuTimer.hpp
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#pragma once
|
||||
|
||||
#include "Profiler.hpp"
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
// Measures GPU time of recorded command ranges with timestamp queries and reports it as a profiler section.
|
||||
class GpuTimer {
|
||||
public:
|
||||
static constexpr uint32_t kSlots = 8;
|
||||
|
||||
GpuTimer(
|
||||
VkDevice device, VkPhysicalDevice physicalDevice, uint32_t queueFamily, Profiler& profiler, const char* name
|
||||
);
|
||||
~GpuTimer();
|
||||
|
||||
GpuTimer(const GpuTimer&) = delete;
|
||||
GpuTimer& operator=(const GpuTimer&) = delete;
|
||||
|
||||
void begin(VkCommandBuffer commandBuffer);
|
||||
void end(VkCommandBuffer commandBuffer);
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool isSupported() const { return mPool != VK_NULL_HANDLE; }
|
||||
void collect();
|
||||
|
||||
VkDevice mDevice;
|
||||
Profiler& mProfiler;
|
||||
ProfileId mSection;
|
||||
VkQueryPool mPool = VK_NULL_HANDLE;
|
||||
double mTickNs = 1;
|
||||
uint64_t mValidMask = ~0ull;
|
||||
|
||||
std::vector<bool> mPending;
|
||||
uint32_t mNextSlot = 0;
|
||||
uint32_t mOpenSlot = UINT32_MAX;
|
||||
};
|
||||
}
|
||||
23
GraphicApplication/src/gpu/private/RenderTargetRegistry.cpp
Normal file
23
GraphicApplication/src/gpu/private/RenderTargetRegistry.cpp
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#include "RenderTargetRegistry.hpp"
|
||||
|
||||
namespace tp {
|
||||
|
||||
uint32_t RenderTargetRegistry::registerTarget(const RenderTargetEntry& entry) {
|
||||
const uint32_t id = mNextId++;
|
||||
mEntries[id] = entry;
|
||||
return id;
|
||||
}
|
||||
|
||||
void RenderTargetRegistry::updateTarget(uint32_t id, const RenderTargetEntry& entry) { mEntries[id] = entry; }
|
||||
|
||||
void RenderTargetRegistry::unregisterTarget(uint32_t id) { mEntries.erase(id); }
|
||||
|
||||
bool RenderTargetRegistry::resolve(uint32_t id, RenderTargetEntry& out) const {
|
||||
const auto found = mEntries.find(id);
|
||||
if (found == mEntries.end()) {
|
||||
return false;
|
||||
}
|
||||
out = found->second;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
571
GraphicApplication/src/gpu/private/WindowSurface.cpp
Normal file
571
GraphicApplication/src/gpu/private/WindowSurface.cpp
Normal file
|
|
@ -0,0 +1,571 @@
|
|||
#include "WindowSurface.hpp"
|
||||
|
||||
#include "GpuError.hpp"
|
||||
#include "GpuTimer.hpp"
|
||||
#include "Profiler.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <limits>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t kUndefinedExtent = std::numeric_limits<uint32_t>::max();
|
||||
|
||||
VkExtent2D atLeastOnePixel(VkExtent2D extent) {
|
||||
return { std::max(extent.width, 1u), std::max(extent.height, 1u) };
|
||||
}
|
||||
|
||||
bool isEmpty(VkExtent2D extent) { return extent.width == 0 || extent.height == 0; }
|
||||
|
||||
VkSurfaceCapabilitiesKHR capabilitiesOf(VkPhysicalDevice device, VkSurfaceKHR surface) {
|
||||
VkSurfaceCapabilitiesKHR capabilities{};
|
||||
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &capabilities);
|
||||
return capabilities;
|
||||
}
|
||||
}
|
||||
|
||||
WindowSurface::WindowSurface(GpuDevice& gpu, VulkanSurfaceSource& source, Profiler& profiler) :
|
||||
mGpu(gpu),
|
||||
mDevice(gpu.device()),
|
||||
mSource(source),
|
||||
mProfiler(profiler),
|
||||
mAcquireSection(profiler.section("frame.acquire")),
|
||||
mSubmitSection(profiler.section("frame.submit")) {
|
||||
createSurface();
|
||||
selectCompositeAlpha();
|
||||
createRenderPass();
|
||||
createFrameSlots();
|
||||
mFrameTimer =
|
||||
std::make_unique<GpuTimer>(mDevice, gpu.physicalDevice(), gpu.graphicsQueueFamily(), profiler, "gpu.window");
|
||||
|
||||
const VkExtent2D extent = mSource.framebufferExtent();
|
||||
createSwapchain(atLeastOnePixel(extent));
|
||||
mRequestedExtent = extent;
|
||||
}
|
||||
|
||||
WindowSurface::~WindowSurface() {
|
||||
mGpu.waitIdle();
|
||||
mFrameTimer.reset();
|
||||
destroySwapchain();
|
||||
destroyFrameSlots();
|
||||
if (mRenderPass != VK_NULL_HANDLE) {
|
||||
vkDestroyRenderPass(mDevice, mRenderPass, nullptr);
|
||||
}
|
||||
if (mSurface != VK_NULL_HANDLE) {
|
||||
vkDestroySurfaceKHR(mGpu.instance(), mSurface, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void WindowSurface::createSurface() {
|
||||
mSurface = mSource.createSurface(mGpu.instance());
|
||||
if (mSurface == VK_NULL_HANDLE) {
|
||||
throw GpuError("creating the window surface", VK_ERROR_SURFACE_LOST_KHR);
|
||||
}
|
||||
if (!mGpu.canPresent(mSurface)) {
|
||||
throw GpuError("presenting to the window surface from the graphics queue", VK_ERROR_INCOMPATIBLE_DISPLAY_KHR);
|
||||
}
|
||||
}
|
||||
|
||||
void WindowSurface::selectCompositeAlpha() {
|
||||
const VkCompositeAlphaFlagsKHR supported = capabilitiesOf(mGpu.physicalDevice(), mSurface).supportedCompositeAlpha;
|
||||
|
||||
mCompositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
|
||||
if (!(supported & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)) {
|
||||
for (VkCompositeAlphaFlagBitsKHR candidate : { VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR,
|
||||
VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR,
|
||||
VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR }) {
|
||||
if (supported & candidate) {
|
||||
mCompositeAlpha = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mSource.wantsTranslucency() && (supported & VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR)) {
|
||||
mCompositeAlpha = VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR;
|
||||
mTranslucent = true;
|
||||
}
|
||||
|
||||
mSource.onTranslucencyResolved(mTranslucent);
|
||||
}
|
||||
|
||||
void WindowSurface::createRenderPass() {
|
||||
uint32_t formatCount = 0;
|
||||
vkGetPhysicalDeviceSurfaceFormatsKHR(mGpu.physicalDevice(), mSurface, &formatCount, nullptr);
|
||||
std::vector<VkSurfaceFormatKHR> formats(formatCount);
|
||||
vkGetPhysicalDeviceSurfaceFormatsKHR(mGpu.physicalDevice(), mSurface, &formatCount, formats.data());
|
||||
if (formats.empty()) {
|
||||
throw GpuError("querying surface formats", VK_ERROR_FORMAT_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
mSwapchainFormat = formats.front().format;
|
||||
for (const VkSurfaceFormatKHR& format : formats) {
|
||||
if (format.format == VK_FORMAT_B8G8R8A8_UNORM && format.colorSpace == VK_COLORSPACE_SRGB_NONLINEAR_KHR) {
|
||||
mSwapchainFormat = format.format;
|
||||
break;
|
||||
}
|
||||
}
|
||||
mDepthStencilFormat = selectDepthStencilFormat();
|
||||
|
||||
const std::array<VkAttachmentDescription, 2> attachments = {
|
||||
VkAttachmentDescription{
|
||||
.format = mSwapchainFormat,
|
||||
.samples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
|
||||
.storeOp = VK_ATTACHMENT_STORE_OP_STORE,
|
||||
.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
|
||||
.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
|
||||
},
|
||||
VkAttachmentDescription{
|
||||
.format = mDepthStencilFormat,
|
||||
.samples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
|
||||
.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
|
||||
.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
|
||||
.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
|
||||
},
|
||||
};
|
||||
|
||||
const VkAttachmentReference colorReference{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
|
||||
const VkAttachmentReference depthReference{ 1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL };
|
||||
|
||||
const VkSubpassDescription subpass{
|
||||
.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
.colorAttachmentCount = 1,
|
||||
.pColorAttachments = &colorReference,
|
||||
.pDepthStencilAttachment = &depthReference,
|
||||
};
|
||||
|
||||
const VkSubpassDependency dependency{
|
||||
.srcSubpass = VK_SUBPASS_EXTERNAL,
|
||||
.dstSubpass = 0,
|
||||
.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT,
|
||||
.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT,
|
||||
.srcAccessMask = 0,
|
||||
.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
||||
};
|
||||
|
||||
const VkRenderPassCreateInfo createInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
|
||||
.attachmentCount = (uint32_t) attachments.size(),
|
||||
.pAttachments = attachments.data(),
|
||||
.subpassCount = 1,
|
||||
.pSubpasses = &subpass,
|
||||
.dependencyCount = 1,
|
||||
.pDependencies = &dependency,
|
||||
};
|
||||
requireSuccess(vkCreateRenderPass(mDevice, &createInfo, nullptr, &mRenderPass), "vkCreateRenderPass");
|
||||
}
|
||||
|
||||
VkFormat WindowSurface::selectDepthStencilFormat() const {
|
||||
for (VkFormat format : { VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D16_UNORM_S8_UINT }) {
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(mGpu.physicalDevice(), format, &properties);
|
||||
if (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) {
|
||||
return format;
|
||||
}
|
||||
}
|
||||
throw GpuError("selecting a depth-stencil format", VK_ERROR_FORMAT_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
void WindowSurface::createFrameSlots() {
|
||||
VkCommandBuffer commandBuffers[kFramesInFlight] = {};
|
||||
const VkCommandBufferAllocateInfo allocateInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
|
||||
.commandPool = mGpu.commandPool(),
|
||||
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
|
||||
.commandBufferCount = kFramesInFlight,
|
||||
};
|
||||
requireSuccess(vkAllocateCommandBuffers(mDevice, &allocateInfo, commandBuffers), "vkAllocateCommandBuffers");
|
||||
|
||||
const VkSemaphoreCreateInfo semaphoreInfo{ .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
|
||||
const VkFenceCreateInfo fenceInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
|
||||
.flags = VK_FENCE_CREATE_SIGNALED_BIT,
|
||||
};
|
||||
for (uint32_t index = 0; index < kFramesInFlight; index++) {
|
||||
FrameSlot& slot = mFrameSlots[index];
|
||||
slot.commandBuffer = commandBuffers[index];
|
||||
requireSuccess(vkCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &slot.imageAvailable), "vkCreateSemaphore");
|
||||
requireSuccess(vkCreateFence(mDevice, &fenceInfo, nullptr, &slot.inFlight), "vkCreateFence");
|
||||
}
|
||||
}
|
||||
|
||||
void WindowSurface::destroyFrameSlots() {
|
||||
for (FrameSlot& slot : mFrameSlots) {
|
||||
if (slot.imageAvailable != VK_NULL_HANDLE) {
|
||||
vkDestroySemaphore(mDevice, slot.imageAvailable, nullptr);
|
||||
}
|
||||
if (slot.inFlight != VK_NULL_HANDLE) {
|
||||
vkDestroyFence(mDevice, slot.inFlight, nullptr);
|
||||
}
|
||||
if (slot.commandBuffer != VK_NULL_HANDLE) {
|
||||
vkFreeCommandBuffers(mDevice, mGpu.commandPool(), 1, &slot.commandBuffer);
|
||||
}
|
||||
slot = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Mailbox replaces the queued frame instead of waiting behind it; FIFO is the guaranteed fallback.
|
||||
VkPresentModeKHR WindowSurface::selectPresentMode() const {
|
||||
uint32_t count = 0;
|
||||
vkGetPhysicalDeviceSurfacePresentModesKHR(mGpu.physicalDevice(), mSurface, &count, nullptr);
|
||||
std::vector<VkPresentModeKHR> modes(count);
|
||||
vkGetPhysicalDeviceSurfacePresentModesKHR(mGpu.physicalDevice(), mSurface, &count, modes.data());
|
||||
const bool hasMailbox = std::find(modes.begin(), modes.end(), VK_PRESENT_MODE_MAILBOX_KHR) != modes.end();
|
||||
return hasMailbox ? VK_PRESENT_MODE_MAILBOX_KHR : VK_PRESENT_MODE_FIFO_KHR;
|
||||
}
|
||||
|
||||
void WindowSurface::createSwapchain(VkExtent2D requested, VkSwapchainKHR previous) {
|
||||
const VkSurfaceCapabilitiesKHR capabilities = capabilitiesOf(mGpu.physicalDevice(), mSurface);
|
||||
|
||||
mSwapchainExtent = capabilities.currentExtent;
|
||||
mSurfaceTransform = capabilities.currentTransform;
|
||||
mSurfaceExtent = capabilities.currentExtent;
|
||||
|
||||
VkSurfaceTransformFlagBitsKHR transform = capabilities.currentTransform;
|
||||
const bool canRenderUnrotated = capabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
if (transform != VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR && canRenderUnrotated) {
|
||||
transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
const bool quarterTurn = capabilities.currentTransform &
|
||||
(VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR | VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR);
|
||||
if (quarterTurn && mSwapchainExtent.width != kUndefinedExtent) {
|
||||
std::swap(mSwapchainExtent.width, mSwapchainExtent.height);
|
||||
}
|
||||
}
|
||||
if (mSwapchainExtent.width == kUndefinedExtent) {
|
||||
mSwapchainExtent.width =
|
||||
std::clamp(requested.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width);
|
||||
mSwapchainExtent.height =
|
||||
std::clamp(requested.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height);
|
||||
}
|
||||
|
||||
const VkPresentModeKHR presentMode = selectPresentMode();
|
||||
uint32_t imageCount = capabilities.minImageCount + (presentMode == VK_PRESENT_MODE_MAILBOX_KHR ? 1 : 0);
|
||||
if (capabilities.maxImageCount > 0) {
|
||||
imageCount = std::min(imageCount, capabilities.maxImageCount);
|
||||
}
|
||||
|
||||
const VkSwapchainCreateInfoKHR createInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
|
||||
.surface = mSurface,
|
||||
.minImageCount = imageCount,
|
||||
.imageFormat = mSwapchainFormat,
|
||||
.imageColorSpace = VK_COLORSPACE_SRGB_NONLINEAR_KHR,
|
||||
.imageExtent = mSwapchainExtent,
|
||||
.imageArrayLayers = 1,
|
||||
.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||
.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE,
|
||||
.preTransform = transform,
|
||||
.compositeAlpha = mCompositeAlpha,
|
||||
.presentMode = presentMode,
|
||||
.clipped = VK_TRUE,
|
||||
.oldSwapchain = previous,
|
||||
};
|
||||
const VkResult created = vkCreateSwapchainKHR(mDevice, &createInfo, nullptr, &mSwapchain);
|
||||
if (previous != VK_NULL_HANDLE) {
|
||||
vkDestroySwapchainKHR(mDevice, previous, nullptr);
|
||||
}
|
||||
requireSuccess(created, "vkCreateSwapchainKHR");
|
||||
|
||||
createSwapchainImages();
|
||||
}
|
||||
|
||||
// Each image owns its render-finished semaphore: presentation may still hold it after its frame slot is reused.
|
||||
void WindowSurface::createSwapchainImages() {
|
||||
uint32_t count = 0;
|
||||
vkGetSwapchainImagesKHR(mDevice, mSwapchain, &count, nullptr);
|
||||
mSwapchainImages.resize(count);
|
||||
vkGetSwapchainImagesKHR(mDevice, mSwapchain, &count, mSwapchainImages.data());
|
||||
|
||||
createDepthStencil();
|
||||
|
||||
const VkSemaphoreCreateInfo semaphoreInfo{ .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
|
||||
mSwapchainImageViews.resize(count);
|
||||
mSwapchainFramebuffers.resize(count);
|
||||
mRenderFinishedSemaphores.resize(count);
|
||||
for (uint32_t index = 0; index < count; index++) {
|
||||
const VkImageViewCreateInfo viewInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||
.image = mSwapchainImages[index],
|
||||
.viewType = VK_IMAGE_VIEW_TYPE_2D,
|
||||
.format = mSwapchainFormat,
|
||||
.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 },
|
||||
};
|
||||
requireSuccess(vkCreateImageView(mDevice, &viewInfo, nullptr, &mSwapchainImageViews[index]), "vkCreateImageView");
|
||||
|
||||
const std::array<VkImageView, 2> attachments = { mSwapchainImageViews[index], mDepthStencilView };
|
||||
const VkFramebufferCreateInfo framebufferInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
|
||||
.renderPass = mRenderPass,
|
||||
.attachmentCount = (uint32_t) attachments.size(),
|
||||
.pAttachments = attachments.data(),
|
||||
.width = mSwapchainExtent.width,
|
||||
.height = mSwapchainExtent.height,
|
||||
.layers = 1,
|
||||
};
|
||||
requireSuccess(
|
||||
vkCreateFramebuffer(mDevice, &framebufferInfo, nullptr, &mSwapchainFramebuffers[index]), "vkCreateFramebuffer"
|
||||
);
|
||||
requireSuccess(
|
||||
vkCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &mRenderFinishedSemaphores[index]), "vkCreateSemaphore"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void WindowSurface::createDepthStencil() {
|
||||
const VkImageCreateInfo imageInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
|
||||
.imageType = VK_IMAGE_TYPE_2D,
|
||||
.format = mDepthStencilFormat,
|
||||
.extent = { mSwapchainExtent.width, mSwapchainExtent.height, 1 },
|
||||
.mipLevels = 1,
|
||||
.arrayLayers = 1,
|
||||
.samples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.tiling = VK_IMAGE_TILING_OPTIMAL,
|
||||
.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
};
|
||||
requireSuccess(vkCreateImage(mDevice, &imageInfo, nullptr, &mDepthStencilImage), "vkCreateImage");
|
||||
|
||||
VkMemoryRequirements requirements{};
|
||||
vkGetImageMemoryRequirements(mDevice, mDepthStencilImage, &requirements);
|
||||
const VkMemoryAllocateInfo allocateInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
|
||||
.allocationSize = requirements.size,
|
||||
.memoryTypeIndex = mGpu.findMemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT),
|
||||
};
|
||||
requireSuccess(vkAllocateMemory(mDevice, &allocateInfo, nullptr, &mDepthStencilMemory), "vkAllocateMemory");
|
||||
requireSuccess(vkBindImageMemory(mDevice, mDepthStencilImage, mDepthStencilMemory, 0), "vkBindImageMemory");
|
||||
|
||||
const VkImageViewCreateInfo viewInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||
.image = mDepthStencilImage,
|
||||
.viewType = VK_IMAGE_VIEW_TYPE_2D,
|
||||
.format = mDepthStencilFormat,
|
||||
.subresourceRange = { VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT, 0, 1, 0, 1 },
|
||||
};
|
||||
requireSuccess(vkCreateImageView(mDevice, &viewInfo, nullptr, &mDepthStencilView), "vkCreateImageView");
|
||||
}
|
||||
|
||||
void WindowSurface::destroyDepthStencil() {
|
||||
if (mDepthStencilView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(mDevice, mDepthStencilView, nullptr);
|
||||
}
|
||||
if (mDepthStencilImage != VK_NULL_HANDLE) {
|
||||
vkDestroyImage(mDevice, mDepthStencilImage, nullptr);
|
||||
}
|
||||
if (mDepthStencilMemory != VK_NULL_HANDLE) {
|
||||
vkFreeMemory(mDevice, mDepthStencilMemory, nullptr);
|
||||
}
|
||||
mDepthStencilView = VK_NULL_HANDLE;
|
||||
mDepthStencilImage = VK_NULL_HANDLE;
|
||||
mDepthStencilMemory = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
void WindowSurface::destroySwapchainResources() {
|
||||
destroyDepthStencil();
|
||||
for (VkFramebuffer framebuffer : mSwapchainFramebuffers) {
|
||||
vkDestroyFramebuffer(mDevice, framebuffer, nullptr);
|
||||
}
|
||||
for (VkImageView view : mSwapchainImageViews) {
|
||||
vkDestroyImageView(mDevice, view, nullptr);
|
||||
}
|
||||
for (VkSemaphore semaphore : mRenderFinishedSemaphores) {
|
||||
vkDestroySemaphore(mDevice, semaphore, nullptr);
|
||||
}
|
||||
mSwapchainFramebuffers.clear();
|
||||
mSwapchainImageViews.clear();
|
||||
mRenderFinishedSemaphores.clear();
|
||||
mSwapchainImages.clear();
|
||||
}
|
||||
|
||||
void WindowSurface::destroySwapchain() {
|
||||
destroySwapchainResources();
|
||||
if (mSwapchain != VK_NULL_HANDLE) {
|
||||
vkDestroySwapchainKHR(mDevice, mSwapchain, nullptr);
|
||||
}
|
||||
mSwapchain = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
void WindowSurface::waitForFramesInFlight() {
|
||||
VkFence fences[kFramesInFlight] = {};
|
||||
for (uint32_t index = 0; index < kFramesInFlight; index++) {
|
||||
fences[index] = mFrameSlots[index].inFlight;
|
||||
}
|
||||
vkWaitForFences(mDevice, kFramesInFlight, fences, VK_TRUE, UINT64_MAX);
|
||||
}
|
||||
|
||||
// Only frames using the old views must finish; the old swapchain is handed over so presentation has no gap.
|
||||
void WindowSurface::recreateSwapchain(VkExtent2D requested) {
|
||||
waitForFramesInFlight();
|
||||
vkQueueWaitIdle(mGpu.graphicsQueue());
|
||||
destroySwapchainResources();
|
||||
|
||||
VkSwapchainKHR previous = mSwapchain;
|
||||
mSwapchain = VK_NULL_HANDLE;
|
||||
createSwapchain(atLeastOnePixel(requested), previous);
|
||||
mRequestedExtent = requested;
|
||||
mSwapchainRecreations++;
|
||||
}
|
||||
|
||||
bool WindowSurface::swapchainMatches(VkExtent2D extent) const {
|
||||
return mRequestedExtent.width == extent.width && mRequestedExtent.height == extent.height;
|
||||
}
|
||||
|
||||
bool WindowSurface::surfaceChanged() const {
|
||||
VkSurfaceCapabilitiesKHR capabilities{};
|
||||
if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(mGpu.physicalDevice(), mSurface, &capabilities) != VK_SUCCESS) {
|
||||
return true;
|
||||
}
|
||||
return capabilities.currentTransform != mSurfaceTransform ||
|
||||
capabilities.currentExtent.width != mSurfaceExtent.width ||
|
||||
capabilities.currentExtent.height != mSurfaceExtent.height;
|
||||
}
|
||||
|
||||
std::optional<GpuFrame> WindowSurface::beginFrame() {
|
||||
const VkExtent2D extent = mSource.framebufferExtent();
|
||||
if (isEmpty(extent) || mSurface == VK_NULL_HANDLE) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!swapchainMatches(extent)) {
|
||||
recreateSwapchain(extent);
|
||||
}
|
||||
|
||||
FrameSlot& slot = mFrameSlots[mCurrentSlot];
|
||||
VkResult acquired = VK_SUCCESS;
|
||||
{
|
||||
const ProfileScope scope(mProfiler, mAcquireSection);
|
||||
vkWaitForFences(mDevice, 1, &slot.inFlight, VK_TRUE, UINT64_MAX);
|
||||
acquired =
|
||||
vkAcquireNextImageKHR(mDevice, mSwapchain, UINT64_MAX, slot.imageAvailable, VK_NULL_HANDLE, &mAcquiredImage);
|
||||
}
|
||||
if (acquired == VK_ERROR_OUT_OF_DATE_KHR) {
|
||||
recreateSwapchain(extent);
|
||||
return std::nullopt;
|
||||
}
|
||||
if (acquired != VK_SUBOPTIMAL_KHR) {
|
||||
requireSuccess(acquired, "vkAcquireNextImageKHR");
|
||||
}
|
||||
|
||||
requireSuccess(vkResetFences(mDevice, 1, &slot.inFlight), "vkResetFences");
|
||||
recordFrameStart(slot.commandBuffer, mAcquiredImage);
|
||||
mFrameActive = true;
|
||||
return GpuFrame{ slot.commandBuffer, mCurrentSlot, mSwapchainExtent };
|
||||
}
|
||||
|
||||
void WindowSurface::recordFrameStart(VkCommandBuffer commandBuffer, uint32_t imageIndex) {
|
||||
requireSuccess(vkResetCommandBuffer(commandBuffer, 0), "vkResetCommandBuffer");
|
||||
const VkCommandBufferBeginInfo beginInfo{ .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
|
||||
requireSuccess(vkBeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer");
|
||||
mFrameTimer->begin(commandBuffer);
|
||||
|
||||
std::array<VkClearValue, 2> clearValues{};
|
||||
clearValues[0].color = { { 0.0f, 0.0f, 0.0f, mTranslucent ? 0.0f : 1.0f } };
|
||||
clearValues[1].depthStencil = { 1.0f, 0 };
|
||||
|
||||
const VkRenderPassBeginInfo renderPassInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
|
||||
.renderPass = mRenderPass,
|
||||
.framebuffer = mSwapchainFramebuffers[imageIndex],
|
||||
.renderArea = { .offset = { 0, 0 }, .extent = mSwapchainExtent },
|
||||
.clearValueCount = (uint32_t) clearValues.size(),
|
||||
.pClearValues = clearValues.data(),
|
||||
};
|
||||
vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
|
||||
|
||||
const VkViewport viewport{
|
||||
.x = 0.0f,
|
||||
.y = 0.0f,
|
||||
.width = (float) mSwapchainExtent.width,
|
||||
.height = (float) mSwapchainExtent.height,
|
||||
.minDepth = 0.0f,
|
||||
.maxDepth = 1.0f,
|
||||
};
|
||||
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
|
||||
|
||||
const VkRect2D scissor{ { 0, 0 }, mSwapchainExtent };
|
||||
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
|
||||
}
|
||||
|
||||
void WindowSurface::endFrame() {
|
||||
if (!mFrameActive) {
|
||||
return;
|
||||
}
|
||||
mFrameActive = false;
|
||||
|
||||
const ProfileScope scope(mProfiler, mSubmitSection);
|
||||
FrameSlot& slot = mFrameSlots[mCurrentSlot];
|
||||
vkCmdEndRenderPass(slot.commandBuffer);
|
||||
mFrameTimer->end(slot.commandBuffer);
|
||||
requireSuccess(vkEndCommandBuffer(slot.commandBuffer), "vkEndCommandBuffer");
|
||||
|
||||
VkSemaphore renderFinished = mRenderFinishedSemaphores[mAcquiredImage];
|
||||
const VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||
const VkSubmitInfo submitInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
|
||||
.waitSemaphoreCount = 1,
|
||||
.pWaitSemaphores = &slot.imageAvailable,
|
||||
.pWaitDstStageMask = &waitStage,
|
||||
.commandBufferCount = 1,
|
||||
.pCommandBuffers = &slot.commandBuffer,
|
||||
.signalSemaphoreCount = 1,
|
||||
.pSignalSemaphores = &renderFinished,
|
||||
};
|
||||
requireSuccess(vkQueueSubmit(mGpu.graphicsQueue(), 1, &submitInfo, slot.inFlight), "vkQueueSubmit");
|
||||
|
||||
const VkPresentInfoKHR presentInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
|
||||
.waitSemaphoreCount = 1,
|
||||
.pWaitSemaphores = &renderFinished,
|
||||
.swapchainCount = 1,
|
||||
.pSwapchains = &mSwapchain,
|
||||
.pImageIndices = &mAcquiredImage,
|
||||
};
|
||||
const VkResult presented = vkQueuePresentKHR(mGpu.graphicsQueue(), &presentInfo);
|
||||
mCurrentSlot = (mCurrentSlot + 1) % kFramesInFlight;
|
||||
|
||||
const bool outOfDate = presented == VK_ERROR_OUT_OF_DATE_KHR;
|
||||
if (outOfDate || (presented == VK_SUBOPTIMAL_KHR && surfaceChanged())) {
|
||||
recreateSwapchain(mSource.framebufferExtent());
|
||||
return;
|
||||
}
|
||||
if (presented != VK_SUBOPTIMAL_KHR) {
|
||||
requireSuccess(presented, "vkQueuePresentKHR");
|
||||
}
|
||||
}
|
||||
|
||||
void WindowSurface::syncToSource() {
|
||||
const VkExtent2D extent = mSource.framebufferExtent();
|
||||
if (isEmpty(extent) || mSurface == VK_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
recreateSwapchain(extent);
|
||||
}
|
||||
|
||||
void WindowSurface::releaseSurface() {
|
||||
if (mSurface == VK_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
mGpu.waitIdle();
|
||||
destroySwapchain();
|
||||
mSwapchainExtent = { 0, 0 };
|
||||
vkDestroySurfaceKHR(mGpu.instance(), mSurface, nullptr);
|
||||
mSurface = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
void WindowSurface::recreateSurface() {
|
||||
if (mSurface != VK_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
createSurface();
|
||||
const VkExtent2D extent = mSource.framebufferExtent();
|
||||
createSwapchain(atLeastOnePixel(extent));
|
||||
mRequestedExtent = extent;
|
||||
}
|
||||
}
|
||||
53
GraphicApplication/src/gpu/public/GpuDevice.hpp
Normal file
53
GraphicApplication/src/gpu/public/GpuDevice.hpp
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
#pragma once
|
||||
|
||||
#include "RenderTargetRegistry.hpp"
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
// The Vulkan instance, device and graphics queue shared by every window of the process.
|
||||
class GpuDevice {
|
||||
public:
|
||||
explicit GpuDevice(const std::vector<const char*>& surfaceExtensions);
|
||||
~GpuDevice();
|
||||
|
||||
GpuDevice(const GpuDevice&) = delete;
|
||||
GpuDevice& operator=(const GpuDevice&) = delete;
|
||||
|
||||
[[nodiscard]] VkInstance instance() const { return mInstance; }
|
||||
[[nodiscard]] VkPhysicalDevice physicalDevice() const { return mPhysicalDevice; }
|
||||
[[nodiscard]] VkDevice device() const { return mDevice; }
|
||||
[[nodiscard]] VkQueue graphicsQueue() const { return mGraphicsQueue; }
|
||||
[[nodiscard]] uint32_t graphicsQueueFamily() const { return mGraphicsQueueFamily; }
|
||||
[[nodiscard]] VkCommandPool commandPool() const { return mCommandPool; }
|
||||
[[nodiscard]] const char* deviceName() const { return mProperties.deviceName; }
|
||||
|
||||
[[nodiscard]] bool canPresent(VkSurfaceKHR surface) const;
|
||||
[[nodiscard]] uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) const;
|
||||
|
||||
[[nodiscard]] VkCommandBuffer beginOneShotCommands() const;
|
||||
void endOneShotCommands(VkCommandBuffer commandBuffer) const;
|
||||
|
||||
void waitIdle() const;
|
||||
|
||||
[[nodiscard]] RenderTargetRegistry& renderTargets() { return mRenderTargets; }
|
||||
|
||||
private:
|
||||
void createInstance(const std::vector<const char*>& surfaceExtensions);
|
||||
void selectPhysicalDevice();
|
||||
void createLogicalDevice();
|
||||
void createCommandPool();
|
||||
|
||||
VkInstance mInstance = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice mPhysicalDevice = VK_NULL_HANDLE;
|
||||
VkPhysicalDeviceProperties mProperties{};
|
||||
VkDevice mDevice = VK_NULL_HANDLE;
|
||||
uint32_t mGraphicsQueueFamily = UINT32_MAX;
|
||||
VkQueue mGraphicsQueue = VK_NULL_HANDLE;
|
||||
VkCommandPool mCommandPool = VK_NULL_HANDLE;
|
||||
RenderTargetRegistry mRenderTargets;
|
||||
};
|
||||
}
|
||||
28
GraphicApplication/src/gpu/public/GpuError.hpp
Normal file
28
GraphicApplication/src/gpu/public/GpuError.hpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace tp {
|
||||
|
||||
// A Vulkan call failed in a way the renderer cannot recover from.
|
||||
class GpuError : public std::runtime_error {
|
||||
public:
|
||||
GpuError(const char* operation, VkResult result) :
|
||||
std::runtime_error(std::string(operation) + " failed: VkResult " + std::to_string((int) result)),
|
||||
mResult(result) {}
|
||||
|
||||
[[nodiscard]] VkResult result() const { return mResult; }
|
||||
|
||||
private:
|
||||
VkResult mResult;
|
||||
};
|
||||
|
||||
inline void requireSuccess(VkResult result, const char* operation) {
|
||||
if (result != VK_SUCCESS) {
|
||||
throw GpuError(operation, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
GraphicApplication/src/gpu/public/RenderTargetRegistry.hpp
Normal file
29
GraphicApplication/src/gpu/public/RenderTargetRegistry.hpp
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace tp {
|
||||
|
||||
struct RenderTargetEntry {
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
class RenderTargetRegistry {
|
||||
public:
|
||||
uint32_t registerTarget(const RenderTargetEntry& entry);
|
||||
void updateTarget(uint32_t id, const RenderTargetEntry& entry);
|
||||
void unregisterTarget(uint32_t id);
|
||||
[[nodiscard]] bool resolve(uint32_t id, RenderTargetEntry& out) const;
|
||||
|
||||
private:
|
||||
uint32_t mNextId = 1;
|
||||
std::unordered_map<uint32_t, RenderTargetEntry> mEntries;
|
||||
};
|
||||
}
|
||||
125
GraphicApplication/src/gpu/public/WindowSurface.hpp
Normal file
125
GraphicApplication/src/gpu/public/WindowSurface.hpp
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
#pragma once
|
||||
|
||||
#include "GpuDevice.hpp"
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
class GpuTimer;
|
||||
class Profiler;
|
||||
|
||||
// The native window a WindowSurface presents into.
|
||||
class VulkanSurfaceSource {
|
||||
public:
|
||||
virtual ~VulkanSurfaceSource() = default;
|
||||
|
||||
[[nodiscard]] virtual VkSurfaceKHR createSurface(VkInstance instance) const = 0;
|
||||
[[nodiscard]] virtual VkExtent2D framebufferExtent() const = 0;
|
||||
[[nodiscard]] virtual bool wantsTranslucency() const = 0;
|
||||
virtual void onTranslucencyResolved(bool translucent) = 0;
|
||||
};
|
||||
|
||||
// The frame being recorded: renderers record into commandBuffer and key per-frame resources by slot.
|
||||
struct GpuFrame {
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
uint32_t slot = 0;
|
||||
VkExtent2D extent{};
|
||||
};
|
||||
|
||||
// Presentation of one window: surface, swapchain, render pass and frame synchronisation.
|
||||
class WindowSurface {
|
||||
public:
|
||||
static constexpr uint32_t kFramesInFlight = 2;
|
||||
|
||||
WindowSurface(GpuDevice& gpu, VulkanSurfaceSource& source, Profiler& profiler);
|
||||
~WindowSurface();
|
||||
|
||||
WindowSurface(const WindowSurface&) = delete;
|
||||
WindowSurface& operator=(const WindowSurface&) = delete;
|
||||
|
||||
[[nodiscard]] GpuDevice& gpu() const { return mGpu; }
|
||||
[[nodiscard]] VkRenderPass renderPass() const { return mRenderPass; }
|
||||
[[nodiscard]] uint32_t imageCount() const { return (uint32_t) mSwapchainImages.size(); }
|
||||
[[nodiscard]] bool isTranslucent() const { return mTranslucent; }
|
||||
[[nodiscard]] bool hasSurface() const { return mSurface != VK_NULL_HANDLE; }
|
||||
[[nodiscard]] uint64_t swapchainRecreations() const { return mSwapchainRecreations; }
|
||||
|
||||
[[nodiscard]] std::optional<GpuFrame> beginFrame();
|
||||
void endFrame();
|
||||
|
||||
void syncToSource();
|
||||
void releaseSurface();
|
||||
void recreateSurface();
|
||||
|
||||
private:
|
||||
struct FrameSlot {
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
VkSemaphore imageAvailable = VK_NULL_HANDLE;
|
||||
VkFence inFlight = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
void createSurface();
|
||||
void selectCompositeAlpha();
|
||||
void createRenderPass();
|
||||
void createFrameSlots();
|
||||
void destroyFrameSlots();
|
||||
|
||||
[[nodiscard]] VkPresentModeKHR selectPresentMode() const;
|
||||
[[nodiscard]] VkFormat selectDepthStencilFormat() const;
|
||||
[[nodiscard]] bool surfaceChanged() const;
|
||||
[[nodiscard]] bool swapchainMatches(VkExtent2D extent) const;
|
||||
|
||||
void createSwapchain(VkExtent2D requested, VkSwapchainKHR previous = VK_NULL_HANDLE);
|
||||
void createSwapchainImages();
|
||||
void createDepthStencil();
|
||||
void destroyDepthStencil();
|
||||
void destroySwapchainResources();
|
||||
void destroySwapchain();
|
||||
void recreateSwapchain(VkExtent2D requested);
|
||||
void waitForFramesInFlight();
|
||||
void recordFrameStart(VkCommandBuffer commandBuffer, uint32_t imageIndex);
|
||||
|
||||
GpuDevice& mGpu;
|
||||
VkDevice mDevice;
|
||||
VulkanSurfaceSource& mSource;
|
||||
Profiler& mProfiler;
|
||||
|
||||
VkSurfaceKHR mSurface = VK_NULL_HANDLE;
|
||||
VkCompositeAlphaFlagBitsKHR mCompositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
|
||||
bool mTranslucent = false;
|
||||
VkSurfaceTransformFlagBitsKHR mSurfaceTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
VkExtent2D mSurfaceExtent{ 0, 0 };
|
||||
VkExtent2D mRequestedExtent{ 0, 0 };
|
||||
|
||||
VkFormat mSwapchainFormat{};
|
||||
VkFormat mDepthStencilFormat{};
|
||||
VkRenderPass mRenderPass = VK_NULL_HANDLE;
|
||||
|
||||
VkSwapchainKHR mSwapchain = VK_NULL_HANDLE;
|
||||
VkExtent2D mSwapchainExtent{};
|
||||
std::vector<VkImage> mSwapchainImages;
|
||||
std::vector<VkImageView> mSwapchainImageViews;
|
||||
std::vector<VkFramebuffer> mSwapchainFramebuffers;
|
||||
std::vector<VkSemaphore> mRenderFinishedSemaphores;
|
||||
uint64_t mSwapchainRecreations = 0;
|
||||
|
||||
VkImage mDepthStencilImage = VK_NULL_HANDLE;
|
||||
VkDeviceMemory mDepthStencilMemory = VK_NULL_HANDLE;
|
||||
VkImageView mDepthStencilView = VK_NULL_HANDLE;
|
||||
|
||||
FrameSlot mFrameSlots[kFramesInFlight];
|
||||
uint32_t mCurrentSlot = 0;
|
||||
uint32_t mAcquiredImage = 0;
|
||||
bool mFrameActive = false;
|
||||
|
||||
std::unique_ptr<GpuTimer> mFrameTimer;
|
||||
uint32_t mAcquireSection = 0;
|
||||
uint32_t mSubmitSection = 0;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
#include "InputCoalescer.hpp"
|
||||
|
||||
using namespace tp;
|
||||
|
||||
namespace {
|
||||
bool isGestureUpdate(const InputEvent& event) {
|
||||
const auto* changed = std::get_if<GestureChanged>(&event);
|
||||
return changed && changed->gesture.phase == GesturePhase::Update;
|
||||
}
|
||||
}
|
||||
|
||||
void InputCoalescer::post(const InputEvent& event) {
|
||||
if (mTap) {
|
||||
mTap->post(event);
|
||||
}
|
||||
mStatistics.received++;
|
||||
|
||||
if (const auto* sampled = std::get_if<PointerSampled>(&event)) {
|
||||
mSample = sampled->sample;
|
||||
return;
|
||||
}
|
||||
if (merge(event)) {
|
||||
return;
|
||||
}
|
||||
mEvents.push_back({ event, mSample });
|
||||
}
|
||||
|
||||
bool InputCoalescer::merge(const InputEvent& event) {
|
||||
if (mEvents.empty()) {
|
||||
return false;
|
||||
}
|
||||
Pending& previous = mEvents.back();
|
||||
|
||||
if (const auto* moved = std::get_if<PointerMoved>(&event)) {
|
||||
auto* last = std::get_if<PointerMoved>(&previous.event);
|
||||
if (!last || mPolicies.pointerMotion == DeliveryPolicy::Every) {
|
||||
return false;
|
||||
}
|
||||
last->position =
|
||||
mPolicies.pointerMotion == DeliveryPolicy::Latest ? moved->position : last->position + moved->position;
|
||||
previous.sample = mSample;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (const auto* scrolled = std::get_if<Scrolled>(&event)) {
|
||||
auto* last = std::get_if<Scrolled>(&previous.event);
|
||||
if (!last || last->source != scrolled->source || mPolicies.scroll == DeliveryPolicy::Every) {
|
||||
return false;
|
||||
}
|
||||
last->delta = mPolicies.scroll == DeliveryPolicy::Latest ? scrolled->delta : last->delta + scrolled->delta;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isGestureUpdate(event) && isGestureUpdate(previous.event)) {
|
||||
accumulate(std::get<GestureChanged>(previous.event).gesture, std::get<GestureChanged>(event).gesture);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool InputCoalescer::carriesSample(const InputEvent& event) {
|
||||
return std::holds_alternative<PointerMoved>(event) || std::holds_alternative<PointerButtonChanged>(event);
|
||||
}
|
||||
|
||||
bool InputCoalescer::isEmptyScroll(const InputEvent& event) {
|
||||
const auto* scrolled = std::get_if<Scrolled>(&event);
|
||||
return scrolled && scrolled->delta == Vec2F{ 0, 0 };
|
||||
}
|
||||
|
||||
void InputCoalescer::deliver(InputSink& sink) {
|
||||
std::vector<Pending> events;
|
||||
events.swap(mEvents);
|
||||
|
||||
for (const Pending& pending : events) {
|
||||
if (isEmptyScroll(pending.event)) {
|
||||
continue;
|
||||
}
|
||||
if (carriesSample(pending.event) && !(pending.sample == mDeliveredSample)) {
|
||||
mDeliveredSample = pending.sample;
|
||||
sink.post(PointerSampled{ pending.sample });
|
||||
}
|
||||
mStatistics.delivered++;
|
||||
sink.post(pending.event);
|
||||
}
|
||||
}
|
||||
413
GraphicApplication/src/input_layer/private/InputRecording.cpp
Normal file
413
GraphicApplication/src/input_layer/private/InputRecording.cpp
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
#include "InputRecording.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
const char* const kDeviceNames[] = { "mouse", "finger", "stylus" };
|
||||
const char* const kTipNames[] = { "pen", "eraser", "brush", "pencil", "airbrush" };
|
||||
const char* const kButtonNames[] = { "left", "right", "middle", "back", "forward" };
|
||||
const char* const kScrollSourceNames[] = { "wheel", "touchpad" };
|
||||
const char* const kKeyActionNames[] = { "press", "release", "repeat" };
|
||||
const char* const kGesturePhaseNames[] = { "begin", "update", "end" };
|
||||
|
||||
template <typename Enum, size_t Count>
|
||||
void writeName(std::ostream& out, const char* const (&names)[Count], Enum value) {
|
||||
out << ' ' << names[(size_t) value];
|
||||
}
|
||||
|
||||
template <typename Enum, size_t Count>
|
||||
bool readName(std::istream& in, const char* const (&names)[Count], Enum& value) {
|
||||
std::string name;
|
||||
in >> name;
|
||||
for (size_t index = 0; index < Count; index++) {
|
||||
if (name == names[index]) {
|
||||
value = (Enum) index;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void writeVector(std::ostream& out, Vec2F value) { out << ' ' << value.x << ' ' << value.y; }
|
||||
|
||||
void readVector(std::istream& in, Vec2F& value) { in >> value.x >> value.y; }
|
||||
|
||||
std::string escapedText(const std::string& text) {
|
||||
std::string escaped;
|
||||
for (const unsigned char byte : text) {
|
||||
if (byte <= ' ' || byte == '%' || byte == 0x7f) {
|
||||
char code[4];
|
||||
std::snprintf(code, sizeof(code), "%%%02X", byte);
|
||||
escaped += code;
|
||||
} else {
|
||||
escaped += (char) byte;
|
||||
}
|
||||
}
|
||||
return escaped;
|
||||
}
|
||||
|
||||
std::optional<std::string> unescapedText(const std::string& escaped) {
|
||||
std::string text;
|
||||
for (size_t index = 0; index < escaped.size(); index++) {
|
||||
if (escaped[index] != '%') {
|
||||
text += escaped[index];
|
||||
continue;
|
||||
}
|
||||
if (index + 2 >= escaped.size()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
text += (char) std::stoi(escaped.substr(index + 1, 2), nullptr, 16);
|
||||
index += 2;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// Recording format of each InputEvent alternative. A missing specialization fails to compile.
|
||||
template <typename Event>
|
||||
struct RecordedFormat;
|
||||
|
||||
template <>
|
||||
struct RecordedFormat<PointerSampled> {
|
||||
static constexpr const char* kName = "sample";
|
||||
|
||||
static void write(std::ostream& out, const PointerSampled& event) {
|
||||
const PointerSample& sample = event.sample;
|
||||
writeName(out, kDeviceNames, sample.device);
|
||||
writeName(out, kTipNames, sample.tip);
|
||||
out << ' ' << sample.pressure;
|
||||
writeVector(out, sample.tilt);
|
||||
out << ' ' << sample.rotation << ' ' << sample.distance << ' ' << sample.timeMs;
|
||||
}
|
||||
|
||||
static bool read(std::istream& in, PointerSampled& event) {
|
||||
PointerSample& sample = event.sample;
|
||||
if (!readName(in, kDeviceNames, sample.device) || !readName(in, kTipNames, sample.tip)) {
|
||||
return false;
|
||||
}
|
||||
in >> sample.pressure;
|
||||
readVector(in, sample.tilt);
|
||||
in >> sample.rotation >> sample.distance >> sample.timeMs;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RecordedFormat<PointerMoved> {
|
||||
static constexpr const char* kName = "move";
|
||||
|
||||
static void write(std::ostream& out, const PointerMoved& event) { writeVector(out, event.position); }
|
||||
|
||||
static bool read(std::istream& in, PointerMoved& event) {
|
||||
readVector(in, event.position);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RecordedFormat<PointerLeft> {
|
||||
static constexpr const char* kName = "leave";
|
||||
|
||||
static void write(std::ostream&, const PointerLeft&) {}
|
||||
|
||||
static bool read(std::istream&, PointerLeft&) { return true; }
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RecordedFormat<PointerButtonChanged> {
|
||||
static constexpr const char* kName = "button";
|
||||
|
||||
static void write(std::ostream& out, const PointerButtonChanged& event) {
|
||||
writeName(out, kButtonNames, event.button);
|
||||
out << ' ' << (int) event.pressed;
|
||||
}
|
||||
|
||||
static bool read(std::istream& in, PointerButtonChanged& event) {
|
||||
int pressed = 0;
|
||||
if (!readName(in, kButtonNames, event.button)) {
|
||||
return false;
|
||||
}
|
||||
in >> pressed;
|
||||
event.pressed = pressed != 0;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RecordedFormat<Scrolled> {
|
||||
static constexpr const char* kName = "scroll";
|
||||
|
||||
static void write(std::ostream& out, const Scrolled& event) {
|
||||
writeName(out, kScrollSourceNames, event.source);
|
||||
writeVector(out, event.delta);
|
||||
}
|
||||
|
||||
static bool read(std::istream& in, Scrolled& event) {
|
||||
if (!readName(in, kScrollSourceNames, event.source)) {
|
||||
return false;
|
||||
}
|
||||
readVector(in, event.delta);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RecordedFormat<ScrollEnded> {
|
||||
static constexpr const char* kName = "scroll-end";
|
||||
|
||||
static void write(std::ostream&, const ScrollEnded&) {}
|
||||
|
||||
static bool read(std::istream&, ScrollEnded&) { return true; }
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RecordedFormat<KeyChanged> {
|
||||
static constexpr const char* kName = "key";
|
||||
|
||||
static void write(std::ostream& out, const KeyChanged& event) {
|
||||
out << ' ' << (unsigned) event.key;
|
||||
writeName(out, kKeyActionNames, event.action);
|
||||
}
|
||||
|
||||
static bool read(std::istream& in, KeyChanged& event) {
|
||||
unsigned code = 0;
|
||||
in >> code;
|
||||
if (code >= kKeyCodeLimit) {
|
||||
return false;
|
||||
}
|
||||
event.key = (Key) code;
|
||||
return readName(in, kKeyActionNames, event.action);
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RecordedFormat<TextEntered> {
|
||||
static constexpr const char* kName = "text";
|
||||
|
||||
static void write(std::ostream& out, const TextEntered& event) { out << ' ' << escapedText(event.utf8); }
|
||||
|
||||
static bool read(std::istream& in, TextEntered& event) {
|
||||
std::string escaped;
|
||||
in >> escaped;
|
||||
const std::optional<std::string> text = unescapedText(escaped);
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
event.utf8 = *text;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RecordedFormat<KeyboardFocusChanged> {
|
||||
static constexpr const char* kName = "focus";
|
||||
|
||||
static void write(std::ostream& out, const KeyboardFocusChanged& event) { out << ' ' << (int) event.focused; }
|
||||
|
||||
static bool read(std::istream& in, KeyboardFocusChanged& event) {
|
||||
int focused = 0;
|
||||
in >> focused;
|
||||
event.focused = focused != 0;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RecordedFormat<GestureChanged> {
|
||||
static constexpr const char* kName = "gesture";
|
||||
|
||||
static void write(std::ostream& out, const GestureChanged& event) {
|
||||
const TouchGesture& gesture = event.gesture;
|
||||
writeName(out, kGesturePhaseNames, gesture.phase);
|
||||
writeVector(out, gesture.center);
|
||||
writeVector(out, gesture.pan);
|
||||
out << ' ' << gesture.scale << ' ' << gesture.rotation << ' ' << gesture.tapFingers;
|
||||
}
|
||||
|
||||
static bool read(std::istream& in, GestureChanged& event) {
|
||||
TouchGesture& gesture = event.gesture;
|
||||
if (!readName(in, kGesturePhaseNames, gesture.phase)) {
|
||||
return false;
|
||||
}
|
||||
readVector(in, gesture.center);
|
||||
readVector(in, gesture.pan);
|
||||
in >> gesture.scale >> gesture.rotation >> gesture.tapFingers;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
template <size_t... Indices>
|
||||
std::optional<InputEvent> readEventOf(const std::string& name, std::istream& in, std::index_sequence<Indices...>) {
|
||||
std::optional<InputEvent> result;
|
||||
const auto tryAlternative = [&]<size_t Index>() {
|
||||
using Event = std::variant_alternative_t<Index, InputEvent>;
|
||||
if (result || name != RecordedFormat<Event>::kName) {
|
||||
return;
|
||||
}
|
||||
Event event;
|
||||
if (RecordedFormat<Event>::read(in, event)) {
|
||||
result = event;
|
||||
}
|
||||
};
|
||||
(tryAlternative.template operator()<Indices>(), ...);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<InputEvent> readEvent(const std::string& name, std::istream& in) {
|
||||
return readEventOf(name, in, std::make_index_sequence<std::variant_size_v<InputEvent>>{});
|
||||
}
|
||||
|
||||
void writeEvent(std::ostream& out, const InputEvent& event) {
|
||||
std::visit(
|
||||
[&out]<typename Event>(const Event& alternative) {
|
||||
out << RecordedFormat<Event>::kName;
|
||||
RecordedFormat<Event>::write(out, alternative);
|
||||
},
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
bool isPointerActivity(const InputEvent& event) {
|
||||
return std::holds_alternative<PointerSampled>(event) || std::holds_alternative<PointerMoved>(event) ||
|
||||
std::holds_alternative<PointerButtonChanged>(event) || std::holds_alternative<PointerLeft>(event);
|
||||
}
|
||||
|
||||
double steadyMs() {
|
||||
const auto now = std::chrono::steady_clock::now().time_since_epoch();
|
||||
return std::chrono::duration<double, std::milli>(now).count();
|
||||
}
|
||||
}
|
||||
|
||||
std::string InputRecording::serialize() const {
|
||||
std::ostringstream text;
|
||||
text << std::setprecision(9);
|
||||
text << kHeader << "\n";
|
||||
text << "window " << windowSize.x << ' ' << windowSize.y << "\n";
|
||||
|
||||
for (const RecordedInput& recorded : events) {
|
||||
text << std::fixed << std::setprecision(3) << recorded.timeMs << ' ' << std::defaultfloat << std::setprecision(9);
|
||||
writeEvent(text, recorded.event);
|
||||
text << "\n";
|
||||
}
|
||||
return text.str();
|
||||
}
|
||||
|
||||
std::optional<InputRecording> InputRecording::parse(const std::string& text) {
|
||||
std::istringstream input(text);
|
||||
std::string line;
|
||||
if (!std::getline(input, line) || line != kHeader) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
InputRecording recording;
|
||||
while (std::getline(input, line)) {
|
||||
if (line.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::istringstream fields(line);
|
||||
std::string first;
|
||||
fields >> first;
|
||||
if (first == "window") {
|
||||
readVector(fields, recording.windowSize);
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string name;
|
||||
fields >> name;
|
||||
const std::optional<InputEvent> event = readEvent(name, fields);
|
||||
if (!event || fields.fail()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
recording.events.push_back({ std::stod(first), *event });
|
||||
}
|
||||
return recording;
|
||||
}
|
||||
|
||||
bool InputRecording::save(const std::string& path) const {
|
||||
std::ofstream file(path);
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
file << serialize();
|
||||
return (bool) file;
|
||||
}
|
||||
|
||||
std::optional<InputRecording> InputRecording::load(const std::string& path) {
|
||||
std::ifstream file(path);
|
||||
if (!file) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::stringstream text;
|
||||
text << file.rdbuf();
|
||||
return parse(text.str());
|
||||
}
|
||||
|
||||
void InputRecording::replayInto(InputSink& sink) const {
|
||||
for (const RecordedInput& recorded : events) {
|
||||
sink.post(recorded.event);
|
||||
}
|
||||
}
|
||||
|
||||
InputRecording InputRecording::translated(Vec2F offset) const {
|
||||
InputRecording moved = *this;
|
||||
for (RecordedInput& recorded : moved.events) {
|
||||
if (auto* pointer = std::get_if<PointerMoved>(&recorded.event)) {
|
||||
pointer->position += offset;
|
||||
}
|
||||
if (auto* gesture = std::get_if<GestureChanged>(&recorded.event)) {
|
||||
gesture->gesture.center += offset;
|
||||
}
|
||||
}
|
||||
return moved;
|
||||
}
|
||||
|
||||
InputRecording InputRecording::slicedToPointerActivity() const {
|
||||
InputRecording sliced;
|
||||
sliced.windowSize = windowSize;
|
||||
|
||||
size_t first = events.size();
|
||||
size_t last = 0;
|
||||
for (size_t index = 0; index < events.size(); index++) {
|
||||
if (!std::holds_alternative<PointerButtonChanged>(events[index].event)) {
|
||||
continue;
|
||||
}
|
||||
first = std::min(first, index);
|
||||
last = index;
|
||||
}
|
||||
if (first >= events.size()) {
|
||||
return sliced;
|
||||
}
|
||||
|
||||
while (first > 0 && isPointerActivity(events[first - 1].event) &&
|
||||
!std::holds_alternative<PointerButtonChanged>(events[first - 1].event)) {
|
||||
first--;
|
||||
}
|
||||
|
||||
const double origin = events[first].timeMs;
|
||||
for (size_t index = first; index <= last; index++) {
|
||||
if (!isPointerActivity(events[index].event)) {
|
||||
continue;
|
||||
}
|
||||
sliced.events.push_back({ events[index].timeMs - origin, events[index].event });
|
||||
}
|
||||
return sliced;
|
||||
}
|
||||
|
||||
InputRecorder::InputRecorder(Clock clock) :
|
||||
mClock(clock ? std::move(clock) : Clock(steadyMs)) {}
|
||||
|
||||
void InputRecorder::post(const InputEvent& event) {
|
||||
const double now = mClock();
|
||||
if (mStartMs < 0) {
|
||||
mStartMs = now;
|
||||
}
|
||||
mRecording.events.push_back({ now - mStartMs, event });
|
||||
}
|
||||
}
|
||||
134
GraphicApplication/src/input_layer/private/InputState.cpp
Normal file
134
GraphicApplication/src/input_layer/private/InputState.cpp
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
#include "InputState.hpp"
|
||||
|
||||
using namespace tp;
|
||||
|
||||
namespace {
|
||||
void press(ButtonState& state) {
|
||||
if (!state.down) {
|
||||
state.pressed = true;
|
||||
}
|
||||
state.down = true;
|
||||
}
|
||||
|
||||
void release(ButtonState& state) {
|
||||
if (state.down) {
|
||||
state.released = true;
|
||||
}
|
||||
state.down = false;
|
||||
}
|
||||
}
|
||||
|
||||
halnf InputState::pointerPressure() const {
|
||||
if (mPointerSample.isStylus()) {
|
||||
return mPointerSample.pressure;
|
||||
}
|
||||
return button(PointerButton::Left).down ? 1.0f : 0.0f;
|
||||
}
|
||||
|
||||
void InputState::beginFrame(std::vector<InputEvent> events) {
|
||||
mPreviousPointer = mPointer;
|
||||
mScrollDelta = { 0, 0 };
|
||||
mContinuousScroll = false;
|
||||
mScrollEnded = false;
|
||||
|
||||
for (ButtonState& state : mPointerButtons) {
|
||||
state.pressed = false;
|
||||
state.released = false;
|
||||
}
|
||||
for (ButtonState& state : mKeys) {
|
||||
state.pressed = false;
|
||||
state.released = false;
|
||||
}
|
||||
|
||||
mFrameEvents = std::move(events);
|
||||
for (const InputEvent& event : mFrameEvents) {
|
||||
apply(event);
|
||||
}
|
||||
}
|
||||
|
||||
void InputState::apply(const InputEvent& event) {
|
||||
std::visit(
|
||||
InputEventVisitor{
|
||||
[this](const PointerSampled& sampled) { mPointerSample = sampled.sample; },
|
||||
[this](const PointerMoved& moved) {
|
||||
mPointer = moved.position;
|
||||
mPointerInside = true;
|
||||
},
|
||||
[this](const PointerLeft&) { mPointerInside = false; },
|
||||
[this](const PointerButtonChanged& changed) {
|
||||
ButtonState& state = mPointerButtons[(size_t) changed.button];
|
||||
changed.pressed ? press(state) : release(state);
|
||||
},
|
||||
[this](const Scrolled& scrolled) {
|
||||
mScrollDelta += scrolled.delta;
|
||||
mContinuousScroll = scrolled.source == ScrollSource::Touchpad;
|
||||
},
|
||||
[this](const ScrollEnded&) { mScrollEnded = true; },
|
||||
[this](const KeyChanged& changed) {
|
||||
ButtonState& state = mKeys[(size_t) changed.key];
|
||||
switch (changed.action) {
|
||||
case KeyAction::Press: press(state); break;
|
||||
case KeyAction::Release: release(state); break;
|
||||
case KeyAction::Repeat: break;
|
||||
}
|
||||
},
|
||||
[](const TextEntered&) {},
|
||||
[](const KeyboardFocusChanged&) {},
|
||||
[](const GestureChanged&) {},
|
||||
},
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
void InputQueue::post(const InputEvent& event) {
|
||||
if (std::holds_alternative<PointerLeft>(event)) {
|
||||
releaseHeldPointerButtons();
|
||||
}
|
||||
if (const auto* focus = std::get_if<KeyboardFocusChanged>(&event); focus && !focus->focused) {
|
||||
releaseHeldKeys();
|
||||
}
|
||||
if (!tracksHeldInput(event)) {
|
||||
return;
|
||||
}
|
||||
mPendingEvents.push_back(event);
|
||||
}
|
||||
|
||||
bool InputQueue::tracksHeldInput(const InputEvent& event) {
|
||||
if (const auto* changed = std::get_if<PointerButtonChanged>(&event)) {
|
||||
if (changed->pressed) {
|
||||
mHeldPointerButtons.insert(changed->button);
|
||||
return true;
|
||||
}
|
||||
return mHeldPointerButtons.erase(changed->button) > 0;
|
||||
}
|
||||
if (const auto* changed = std::get_if<KeyChanged>(&event)) {
|
||||
if (changed->action == KeyAction::Press) {
|
||||
mHeldKeys.insert(changed->key);
|
||||
} else if (changed->action == KeyAction::Release) {
|
||||
mHeldKeys.erase(changed->key);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void InputQueue::releaseHeldPointerButtons() {
|
||||
const std::set<PointerButton> held = std::move(mHeldPointerButtons);
|
||||
mHeldPointerButtons.clear();
|
||||
for (PointerButton button : held) {
|
||||
mPendingEvents.push_back(PointerButtonChanged{ button, false });
|
||||
}
|
||||
}
|
||||
|
||||
void InputQueue::releaseHeldKeys() {
|
||||
const std::set<Key> held = std::move(mHeldKeys);
|
||||
mHeldKeys.clear();
|
||||
for (Key key : held) {
|
||||
mPendingEvents.push_back(KeyChanged{ key, KeyAction::Release });
|
||||
}
|
||||
}
|
||||
|
||||
void InputQueue::applyPendingEvents() {
|
||||
std::vector<InputEvent> events;
|
||||
events.swap(mPendingEvents);
|
||||
mState.beginFrame(std::move(events));
|
||||
}
|
||||
122
GraphicApplication/src/input_layer/private/TouchGesture.cpp
Normal file
122
GraphicApplication/src/input_layer/private/TouchGesture.cpp
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
#include "TouchGesture.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
constexpr halnf kMinimumSpan = 1.0f;
|
||||
|
||||
}
|
||||
|
||||
void TouchGestureTracker::track(const TouchPoint& first, const TouchPoint& second) {
|
||||
const Vec2F offset = second.position - first.position;
|
||||
mFirst = first.id;
|
||||
mSecond = second.id;
|
||||
mCenter = (first.position + second.position) * 0.5f;
|
||||
mDistance = std::sqrt(offset.x * offset.x + offset.y * offset.y);
|
||||
mAngle = std::atan2(offset.y, offset.x);
|
||||
}
|
||||
|
||||
void TouchGestureTracker::followTap(const std::vector<TouchPoint>& touches, uint32_t timeMs) {
|
||||
if (mTapStarts.empty() && !touches.empty()) {
|
||||
mTapStartMs = timeMs;
|
||||
mTapStillPossible = true;
|
||||
}
|
||||
for (const TouchPoint& touch : touches) {
|
||||
auto start = std::find_if(mTapStarts.begin(), mTapStarts.end(), [&](const TouchPoint& known) {
|
||||
return known.id == touch.id;
|
||||
});
|
||||
if (start == mTapStarts.end()) {
|
||||
mTapStarts.push_back(touch);
|
||||
continue;
|
||||
}
|
||||
const Vec2F travel = touch.position - start->position;
|
||||
if (travel.x * travel.x + travel.y * travel.y > kTapMaximumTravel * kTapMaximumTravel) {
|
||||
mTapStillPossible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int32_t TouchGestureTracker::finishedTapFingers(uint32_t timeMs) const {
|
||||
const bool quick = timeMs - mTapStartMs <= kTapMaximumMs;
|
||||
return mTapStillPossible && quick && mTapStarts.size() >= 2 ? (int32_t) mTapStarts.size() : 0;
|
||||
}
|
||||
|
||||
void TouchGestureTracker::update(
|
||||
const std::vector<TouchPoint>& touches, std::vector<TouchGesture>& gestures, uint32_t timeMs
|
||||
) {
|
||||
followTap(touches, timeMs);
|
||||
if (touches.empty()) {
|
||||
const int32_t tapFingers = finishedTapFingers(timeMs);
|
||||
mTapStarts.clear();
|
||||
cancel(gestures);
|
||||
if (!gestures.empty() && gestures.back().phase == GesturePhase::End) {
|
||||
gestures.back().tapFingers = tapFingers;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (touches.size() < 2) {
|
||||
if (finishedTapFingers(timeMs) == 0) {
|
||||
cancel(gestures);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const TouchPoint& first = touches[0];
|
||||
const TouchPoint& second = touches[1];
|
||||
|
||||
if (!mActive) {
|
||||
track(first, second);
|
||||
mActive = true;
|
||||
gestures.push_back({ GesturePhase::Begin, mCenter });
|
||||
return;
|
||||
}
|
||||
|
||||
if (first.id != mFirst || second.id != mSecond) {
|
||||
track(first, second);
|
||||
return;
|
||||
}
|
||||
|
||||
const Vec2F previousCenter = mCenter;
|
||||
const halnf previousDistance = mDistance;
|
||||
const halnf previousAngle = mAngle;
|
||||
track(first, second);
|
||||
|
||||
TouchGesture gesture;
|
||||
gesture.phase = GesturePhase::Update;
|
||||
gesture.center = mCenter;
|
||||
gesture.pan = mCenter - previousCenter;
|
||||
gesture.scale = previousDistance >= kMinimumSpan && mDistance >= kMinimumSpan ? mDistance / previousDistance : 1;
|
||||
gesture.rotation = wrapAngle(mAngle - previousAngle);
|
||||
gestures.push_back(gesture);
|
||||
}
|
||||
|
||||
void TouchGestureTracker::cancel(std::vector<TouchGesture>& gestures) {
|
||||
if (!mActive) {
|
||||
return;
|
||||
}
|
||||
mActive = false;
|
||||
gestures.push_back({ GesturePhase::End, mCenter });
|
||||
}
|
||||
|
||||
halnf wrapAngle(halnf radians) {
|
||||
constexpr halnf pi = std::numbers::pi_v<halnf>;
|
||||
while (radians > pi) {
|
||||
radians -= 2 * pi;
|
||||
}
|
||||
while (radians < -pi) {
|
||||
radians += 2 * pi;
|
||||
}
|
||||
return radians;
|
||||
}
|
||||
|
||||
void accumulate(TouchGesture& into, const TouchGesture& next) {
|
||||
into.center = next.center;
|
||||
into.pan += next.pan;
|
||||
into.scale *= next.scale;
|
||||
into.rotation = wrapAngle(into.rotation + next.rotation);
|
||||
}
|
||||
}
|
||||
52
GraphicApplication/src/input_layer/public/InputCoalescer.hpp
Normal file
52
GraphicApplication/src/input_layer/public/InputCoalescer.hpp
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputSink.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
enum class DeliveryPolicy { Every, Latest, Accumulate };
|
||||
|
||||
struct InputDeliveryPolicies {
|
||||
DeliveryPolicy pointerMotion = DeliveryPolicy::Latest;
|
||||
DeliveryPolicy scroll = DeliveryPolicy::Accumulate;
|
||||
};
|
||||
|
||||
struct CoalescingStatistics {
|
||||
uint64_t received = 0;
|
||||
uint64_t delivered = 0;
|
||||
};
|
||||
|
||||
// Backends post raw events here; they are batched and delivered once per frame.
|
||||
class InputCoalescer final : public InputSink {
|
||||
public:
|
||||
void post(const InputEvent& event) override;
|
||||
|
||||
void setTap(InputSink* tap) { mTap = tap; }
|
||||
void setPolicies(const InputDeliveryPolicies& policies) { mPolicies = policies; }
|
||||
|
||||
[[nodiscard]] bool isEmpty() const { return mEvents.empty(); }
|
||||
void deliver(InputSink& sink);
|
||||
|
||||
[[nodiscard]] const CoalescingStatistics& statistics() const { return mStatistics; }
|
||||
|
||||
private:
|
||||
struct Pending {
|
||||
InputEvent event;
|
||||
PointerSample sample;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool merge(const InputEvent& event);
|
||||
[[nodiscard]] static bool carriesSample(const InputEvent& event);
|
||||
[[nodiscard]] static bool isEmptyScroll(const InputEvent& event);
|
||||
|
||||
InputSink* mTap = nullptr;
|
||||
InputDeliveryPolicies mPolicies;
|
||||
std::vector<Pending> mEvents;
|
||||
PointerSample mSample;
|
||||
PointerSample mDeliveredSample;
|
||||
CoalescingStatistics mStatistics;
|
||||
};
|
||||
}
|
||||
137
GraphicApplication/src/input_layer/public/InputCodes.hpp
Normal file
137
GraphicApplication/src/input_layer/public/InputCodes.hpp
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace tp {
|
||||
|
||||
enum class PointerButton : uint8_t { Left, Right, Middle, Back, Forward };
|
||||
|
||||
inline constexpr size_t kPointerButtonCount = 5;
|
||||
|
||||
enum class Key : uint16_t {
|
||||
Space = 32,
|
||||
Apostrophe = 39,
|
||||
Comma = 44,
|
||||
Minus = 45,
|
||||
Period = 46,
|
||||
Slash = 47,
|
||||
Digit0 = 48,
|
||||
Digit1 = 49,
|
||||
Digit2 = 50,
|
||||
Digit3 = 51,
|
||||
Digit4 = 52,
|
||||
Digit5 = 53,
|
||||
Digit6 = 54,
|
||||
Digit7 = 55,
|
||||
Digit8 = 56,
|
||||
Digit9 = 57,
|
||||
Semicolon = 59,
|
||||
Equal = 61,
|
||||
A = 65,
|
||||
B = 66,
|
||||
C = 67,
|
||||
D = 68,
|
||||
E = 69,
|
||||
F = 70,
|
||||
G = 71,
|
||||
H = 72,
|
||||
I = 73,
|
||||
J = 74,
|
||||
K = 75,
|
||||
L = 76,
|
||||
M = 77,
|
||||
N = 78,
|
||||
O = 79,
|
||||
P = 80,
|
||||
Q = 81,
|
||||
R = 82,
|
||||
S = 83,
|
||||
T = 84,
|
||||
U = 85,
|
||||
V = 86,
|
||||
W = 87,
|
||||
X = 88,
|
||||
Y = 89,
|
||||
Z = 90,
|
||||
LeftBracket = 91,
|
||||
Backslash = 92,
|
||||
RightBracket = 93,
|
||||
GraveAccent = 96,
|
||||
World1 = 161,
|
||||
World2 = 162,
|
||||
|
||||
Escape = 256,
|
||||
Enter = 257,
|
||||
Tab = 258,
|
||||
Backspace = 259,
|
||||
Insert = 260,
|
||||
Delete = 261,
|
||||
Right = 262,
|
||||
Left = 263,
|
||||
Down = 264,
|
||||
Up = 265,
|
||||
PageUp = 266,
|
||||
PageDown = 267,
|
||||
Home = 268,
|
||||
End = 269,
|
||||
CapsLock = 280,
|
||||
ScrollLock = 281,
|
||||
NumLock = 282,
|
||||
PrintScreen = 283,
|
||||
Pause = 284,
|
||||
F1 = 290,
|
||||
F2 = 291,
|
||||
F3 = 292,
|
||||
F4 = 293,
|
||||
F5 = 294,
|
||||
F6 = 295,
|
||||
F7 = 296,
|
||||
F8 = 297,
|
||||
F9 = 298,
|
||||
F10 = 299,
|
||||
F11 = 300,
|
||||
F12 = 301,
|
||||
F13 = 302,
|
||||
F14 = 303,
|
||||
F15 = 304,
|
||||
F16 = 305,
|
||||
F17 = 306,
|
||||
F18 = 307,
|
||||
F19 = 308,
|
||||
F20 = 309,
|
||||
F21 = 310,
|
||||
F22 = 311,
|
||||
F23 = 312,
|
||||
F24 = 313,
|
||||
F25 = 314,
|
||||
Keypad0 = 320,
|
||||
Keypad1 = 321,
|
||||
Keypad2 = 322,
|
||||
Keypad3 = 323,
|
||||
Keypad4 = 324,
|
||||
Keypad5 = 325,
|
||||
Keypad6 = 326,
|
||||
Keypad7 = 327,
|
||||
Keypad8 = 328,
|
||||
Keypad9 = 329,
|
||||
KeypadDecimal = 330,
|
||||
KeypadDivide = 331,
|
||||
KeypadMultiply = 332,
|
||||
KeypadSubtract = 333,
|
||||
KeypadAdd = 334,
|
||||
KeypadEnter = 335,
|
||||
KeypadEqual = 336,
|
||||
LeftShift = 340,
|
||||
LeftControl = 341,
|
||||
LeftAlt = 342,
|
||||
LeftSuper = 343,
|
||||
RightShift = 344,
|
||||
RightControl = 345,
|
||||
RightAlt = 346,
|
||||
RightSuper = 347,
|
||||
Menu = 348,
|
||||
};
|
||||
|
||||
inline constexpr size_t kKeyCodeLimit = 349;
|
||||
}
|
||||
93
GraphicApplication/src/input_layer/public/InputEvent.hpp
Normal file
93
GraphicApplication/src/input_layer/public/InputEvent.hpp
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputCodes.hpp"
|
||||
#include "PointerSample.hpp"
|
||||
#include "TouchGesture.hpp"
|
||||
#include "Vec.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
namespace tp {
|
||||
|
||||
enum class ScrollSource { Wheel, Touchpad };
|
||||
|
||||
enum class KeyAction { Press, Release, Repeat };
|
||||
|
||||
struct PointerSampled {
|
||||
PointerSample sample;
|
||||
|
||||
[[nodiscard]] bool operator==(const PointerSampled& other) const = default;
|
||||
};
|
||||
|
||||
struct PointerMoved {
|
||||
Vec2F position = { 0, 0 };
|
||||
|
||||
[[nodiscard]] bool operator==(const PointerMoved& other) const = default;
|
||||
};
|
||||
|
||||
struct PointerLeft {
|
||||
[[nodiscard]] bool operator==(const PointerLeft& other) const = default;
|
||||
};
|
||||
|
||||
struct PointerButtonChanged {
|
||||
PointerButton button = PointerButton::Left;
|
||||
bool pressed = false;
|
||||
|
||||
[[nodiscard]] bool operator==(const PointerButtonChanged& other) const = default;
|
||||
};
|
||||
|
||||
struct Scrolled {
|
||||
Vec2F delta = { 0, 0 };
|
||||
ScrollSource source = ScrollSource::Wheel;
|
||||
|
||||
[[nodiscard]] bool operator==(const Scrolled& other) const = default;
|
||||
};
|
||||
|
||||
struct ScrollEnded {
|
||||
[[nodiscard]] bool operator==(const ScrollEnded& other) const = default;
|
||||
};
|
||||
|
||||
struct KeyChanged {
|
||||
Key key = Key::Space;
|
||||
KeyAction action = KeyAction::Press;
|
||||
|
||||
[[nodiscard]] bool operator==(const KeyChanged& other) const = default;
|
||||
};
|
||||
|
||||
struct TextEntered {
|
||||
std::string utf8;
|
||||
|
||||
[[nodiscard]] bool operator==(const TextEntered& other) const = default;
|
||||
};
|
||||
|
||||
struct KeyboardFocusChanged {
|
||||
bool focused = false;
|
||||
|
||||
[[nodiscard]] bool operator==(const KeyboardFocusChanged& other) const = default;
|
||||
};
|
||||
|
||||
struct GestureChanged {
|
||||
TouchGesture gesture;
|
||||
|
||||
[[nodiscard]] bool operator==(const GestureChanged& other) const = default;
|
||||
};
|
||||
|
||||
// The one declaration of every input event; sinks, the coalescer, input state and recordings derive from it.
|
||||
using InputEvent = std::variant<
|
||||
PointerSampled,
|
||||
PointerMoved,
|
||||
PointerLeft,
|
||||
PointerButtonChanged,
|
||||
Scrolled,
|
||||
ScrollEnded,
|
||||
KeyChanged,
|
||||
TextEntered,
|
||||
KeyboardFocusChanged,
|
||||
GestureChanged>;
|
||||
|
||||
template <typename... Handlers>
|
||||
struct InputEventVisitor : Handlers... {
|
||||
using Handlers::operator()...;
|
||||
};
|
||||
}
|
||||
54
GraphicApplication/src/input_layer/public/InputRecording.hpp
Normal file
54
GraphicApplication/src/input_layer/public/InputRecording.hpp
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputSink.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
struct RecordedInput {
|
||||
double timeMs = 0;
|
||||
InputEvent event;
|
||||
|
||||
[[nodiscard]] bool operator==(const RecordedInput& other) const = default;
|
||||
};
|
||||
|
||||
struct InputRecording {
|
||||
static constexpr const char* kHeader = "tp-input-recording 2";
|
||||
|
||||
Vec2F windowSize = { 0, 0 };
|
||||
std::vector<RecordedInput> events;
|
||||
|
||||
[[nodiscard]] double durationMs() const { return events.empty() ? 0.0 : events.back().timeMs; }
|
||||
[[nodiscard]] std::string serialize() const;
|
||||
[[nodiscard]] static std::optional<InputRecording> parse(const std::string& text);
|
||||
|
||||
bool save(const std::string& path) const;
|
||||
[[nodiscard]] static std::optional<InputRecording> load(const std::string& path);
|
||||
|
||||
void replayInto(InputSink& sink) const;
|
||||
[[nodiscard]] InputRecording translated(Vec2F offset) const;
|
||||
[[nodiscard]] InputRecording slicedToPointerActivity() const;
|
||||
};
|
||||
|
||||
class InputRecorder final : public InputSink {
|
||||
public:
|
||||
using Clock = std::function<double()>;
|
||||
|
||||
explicit InputRecorder(Clock clock = {});
|
||||
|
||||
void setWindowSize(Vec2F size) { mRecording.windowSize = size; }
|
||||
void startClockNow() { mStartMs = mClock(); }
|
||||
[[nodiscard]] const InputRecording& recording() const { return mRecording; }
|
||||
|
||||
void post(const InputEvent& event) override;
|
||||
|
||||
private:
|
||||
Clock mClock;
|
||||
double mStartMs = -1;
|
||||
InputRecording mRecording;
|
||||
};
|
||||
}
|
||||
18
GraphicApplication/src/input_layer/public/InputSink.hpp
Normal file
18
GraphicApplication/src/input_layer/public/InputSink.hpp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputEvent.hpp"
|
||||
|
||||
namespace tp {
|
||||
|
||||
class InputSink {
|
||||
public:
|
||||
virtual ~InputSink() = default;
|
||||
|
||||
virtual void post(const InputEvent& event) = 0;
|
||||
};
|
||||
|
||||
class DiscardingInputSink final : public InputSink {
|
||||
public:
|
||||
void post(const InputEvent&) override {}
|
||||
};
|
||||
}
|
||||
118
GraphicApplication/src/input_layer/public/InputState.hpp
Normal file
118
GraphicApplication/src/input_layer/public/InputState.hpp
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputSink.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
struct ButtonState {
|
||||
bool pressed = false;
|
||||
bool down = false;
|
||||
bool released = false;
|
||||
};
|
||||
|
||||
// The input of one frame: the ordered event log plus the per-frame state derived from it. Read-only for consumers.
|
||||
class InputState {
|
||||
public:
|
||||
[[nodiscard]] const std::vector<InputEvent>& events() const { return mFrameEvents; }
|
||||
|
||||
[[nodiscard]] Vec2F pointer() const { return mPointer; }
|
||||
[[nodiscard]] Vec2F previousPointer() const { return mPreviousPointer; }
|
||||
[[nodiscard]] Vec2F pointerDelta() const { return mPointer - mPreviousPointer; }
|
||||
[[nodiscard]] bool isPointerInside() const { return mPointerInside; }
|
||||
[[nodiscard]] const PointerSample& pointerSample() const { return mPointerSample; }
|
||||
[[nodiscard]] halnf pointerPressure() const;
|
||||
|
||||
[[nodiscard]] const ButtonState& button(PointerButton button) const { return mPointerButtons[(size_t) button]; }
|
||||
[[nodiscard]] const ButtonState& key(Key key) const { return mKeys[(size_t) key]; }
|
||||
|
||||
[[nodiscard]] Vec2F scroll() const { return mScrollDelta; }
|
||||
[[nodiscard]] bool isContinuousScroll() const { return mContinuousScroll; }
|
||||
[[nodiscard]] bool isScrollEnded() const { return mScrollEnded; }
|
||||
|
||||
private:
|
||||
friend class InputQueue;
|
||||
|
||||
void beginFrame(std::vector<InputEvent> events);
|
||||
void apply(const InputEvent& event);
|
||||
|
||||
std::vector<InputEvent> mFrameEvents;
|
||||
|
||||
Vec2F mPointer = { 0, 0 };
|
||||
Vec2F mPreviousPointer = { 0, 0 };
|
||||
bool mPointerInside = false;
|
||||
PointerSample mPointerSample;
|
||||
Vec2F mScrollDelta = { 0, 0 };
|
||||
bool mContinuousScroll = false;
|
||||
bool mScrollEnded = false;
|
||||
|
||||
std::array<ButtonState, kPointerButtonCount> mPointerButtons{};
|
||||
std::array<ButtonState, kKeyCodeLimit> mKeys{};
|
||||
};
|
||||
|
||||
// Collects delivered events between frames and releases held buttons and keys when focus or the pointer is lost.
|
||||
class InputQueue final : public InputSink {
|
||||
public:
|
||||
void post(const InputEvent& event) override;
|
||||
|
||||
[[nodiscard]] size_t pendingEventCount() const { return mPendingEvents.size(); }
|
||||
void applyPendingEvents();
|
||||
|
||||
[[nodiscard]] const InputState& state() const { return mState; }
|
||||
|
||||
private:
|
||||
void releaseHeldPointerButtons();
|
||||
void releaseHeldKeys();
|
||||
[[nodiscard]] bool tracksHeldInput(const InputEvent& event);
|
||||
|
||||
std::vector<InputEvent> mPendingEvents;
|
||||
std::set<PointerButton> mHeldPointerButtons;
|
||||
std::set<Key> mHeldKeys;
|
||||
InputState mState;
|
||||
};
|
||||
|
||||
// A consumer's view of one frame: pointer coordinates relative to an origin, optionally without button/key state.
|
||||
class InputView {
|
||||
public:
|
||||
enum class Buttons { Visible, Hidden };
|
||||
|
||||
explicit InputView(const InputState& state, Vec2F origin = { 0, 0 }, Buttons buttons = Buttons::Visible) :
|
||||
mState(state),
|
||||
mOrigin(origin),
|
||||
mButtons(buttons) {}
|
||||
|
||||
[[nodiscard]] InputView relativeTo(Vec2F origin) const { return InputView(mState, origin, mButtons); }
|
||||
[[nodiscard]] InputView withButtons(Buttons buttons) const { return InputView(mState, mOrigin, buttons); }
|
||||
|
||||
[[nodiscard]] const InputState& state() const { return mState; }
|
||||
|
||||
[[nodiscard]] Vec2F getPointer() const { return mState.pointer() - mOrigin; }
|
||||
[[nodiscard]] Vec2F getPointerPrev() const { return mState.previousPointer() - mOrigin; }
|
||||
[[nodiscard]] Vec2F getPointerDelta() const { return mState.pointerDelta(); }
|
||||
[[nodiscard]] halnf getPointerPressure() const { return areButtonsVisible() ? mState.pointerPressure() : 0; }
|
||||
|
||||
[[nodiscard]] bool isPressed(PointerButton button) const { return visible(mState.button(button)).pressed; }
|
||||
[[nodiscard]] bool isReleased(PointerButton button) const { return visible(mState.button(button)).released; }
|
||||
[[nodiscard]] bool isDown(PointerButton button) const { return visible(mState.button(button)).down; }
|
||||
[[nodiscard]] bool isPressed(Key key) const { return visible(mState.key(key)).pressed; }
|
||||
[[nodiscard]] bool isReleased(Key key) const { return visible(mState.key(key)).released; }
|
||||
[[nodiscard]] bool isDown(Key key) const { return visible(mState.key(key)).down; }
|
||||
|
||||
[[nodiscard]] halnf getScrollX() const { return mState.scroll().x; }
|
||||
[[nodiscard]] halnf getScrollY() const { return mState.scroll().y; }
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool areButtonsVisible() const { return mButtons == Buttons::Visible; }
|
||||
[[nodiscard]] ButtonState visible(const ButtonState& state) const {
|
||||
return areButtonsVisible() ? state : ButtonState{};
|
||||
}
|
||||
|
||||
const InputState& mState;
|
||||
Vec2F mOrigin;
|
||||
Buttons mButtons;
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue