GraphicApplication: Vulkan renderer, native windows, input and frame diagnostics #9
150 changed files with 12488 additions and 1467 deletions
|
|
@ -20,5 +20,5 @@ add_executable(3DEditorApp ${APP_SOURCES})
|
|||
target_link_libraries(3DEditorApp ${PROJECT_NAME})
|
||||
|
||||
file(COPY "../RasterRender/rsc" DESTINATION "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/")
|
||||
file(COPY "../Graphics/rsc" DESTINATION "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/")
|
||||
file(COPY "../GraphicApplication/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();
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ namespace tp {
|
|||
|
||||
~ViewportWidget() override { mCanvas->deleteImageHandle(mImage); }
|
||||
|
||||
void process(const EventHandler& events) override {
|
||||
void process(const InputState& events) override {
|
||||
mEditor->setViewportSize(getAreaT().size);
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ namespace tp {
|
|||
mNavigationReset.setAction( [this]() { mEditor->navigationReset(); });
|
||||
}
|
||||
|
||||
void process(const EventHandler& events) override {
|
||||
void process(const InputState& events) override {
|
||||
DockWidget::process(events);
|
||||
|
||||
if (auto obj = mEditor->getActiveObject()) { // dummy rotate active object
|
||||
|
|
|
|||
|
|
@ -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
|
||||
121
GraphicApplication/CMakeLists.txt
Normal file
121
GraphicApplication/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
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)
|
||||
|
||||
### ---------------------- Render --------------------- ###
|
||||
graphic_application_library(${PROJECT_NAME}Render render)
|
||||
target_link_libraries(${PROJECT_NAME}Render PUBLIC ${PROJECT_NAME}Diagnostics Vulkan::Vulkan)
|
||||
|
||||
### ---------------------- 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}Render)
|
||||
|
||||
### ---------------------- 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 Containers ${PROJECT_NAME}BackendContract PRIVATE ${PROJECT_NAME}Backend)
|
||||
|
||||
### ---------------------- Canvas --------------------- ###
|
||||
graphic_application_library(${PROJECT_NAME}Canvas canvas)
|
||||
target_link_libraries(${PROJECT_NAME}Canvas PUBLIC ${PROJECT_NAME}Window PRIVATE ${PROJECT_NAME}Diagnostics NanovgVk)
|
||||
|
||||
### ---------------------- 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}Canvas ${PROJECT_NAME}DebugUI ${PROJECT_NAME}Diagnostics)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE MODULES_GENERIC_APP_ID="online.ilushabasic.modules" MODULES_GENERIC_APP_NAME="Modules" GRAPHIC_APPLICATION_RESOURCE_DIRECTORY="${CMAKE_BINARY_DIR}/${PROJECT_NAME}/rsc")
|
||||
file(COPY "./rsc" DESTINATION "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/")
|
||||
|
||||
### -------------------------- Examples -------------------------- ###
|
||||
if (NOT ANDROID)
|
||||
add_executable(Example${PROJECT_NAME} ./examples/Example.cpp)
|
||||
target_link_libraries(Example${PROJECT_NAME} ${PROJECT_NAME} Imgui)
|
||||
|
||||
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/diagnostics/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}Diagnostics NanovgVk UnitTest++)
|
||||
add_test(NAME Test${PROJECT_NAME} COMMAND Test${PROJECT_NAME})
|
||||
endif ()
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
|
||||
#include "GraphicApplication.hpp"
|
||||
#include <GL/glew.h>
|
||||
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
|
|
@ -11,13 +9,9 @@ class ExampleApplication : public Application {
|
|||
public:
|
||||
ExampleApplication() = default;
|
||||
|
||||
void processFrame(EventHandler* eventHandler, halnf) override {
|
||||
// example
|
||||
}
|
||||
void processFrame(InputState* inputState, halnf) override {}
|
||||
|
||||
virtual void drawFrame(Canvas* canvas) override {
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
ImGui::ShowDemoWindow();
|
||||
|
||||
drawDebug();
|
||||
|
|
@ -28,7 +22,7 @@ public:
|
|||
virtual ~ExampleApplication() = default;
|
||||
};
|
||||
|
||||
int main() {
|
||||
int main() {
|
||||
ExampleApplication app;
|
||||
app.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,280 @@
|
|||
#include "GraphicApplication.hpp"
|
||||
|
||||
#include "Profiler.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
using namespace tp;
|
||||
|
||||
namespace {
|
||||
std::filesystem::path defaultResourceDirectory() {
|
||||
#ifdef __ANDROID__
|
||||
const char* home = std::getenv("HOME");
|
||||
return home ? std::filesystem::path(home) / "rsc" : std::filesystem::path("rsc");
|
||||
#else
|
||||
return GRAPHIC_APPLICATION_RESOURCE_DIRECTORY;
|
||||
#endif
|
||||
}
|
||||
|
||||
const RGBA kWindowBaseColor = { 0.118f, 0.122f, 0.133f, 1.0f };
|
||||
const RGBA kWindowOutlineColor = { 1.0f, 1.0f, 1.0f, 0.07f };
|
||||
const RGBA kFocusedShadowColor = { 0, 0, 0, 0.42f };
|
||||
const RGBA kUnfocusedShadowColor = { 0, 0, 0, 0.24f };
|
||||
const RGBA kTransparent = { 0, 0, 0, 0 };
|
||||
constexpr halnf kShadowFeatherFraction = 0.9f;
|
||||
constexpr halnf kShadowOffsetY = 3;
|
||||
constexpr halnf kOutlineWidth = 1;
|
||||
}
|
||||
|
||||
WindowIdentity WindowIdentity::generic() { return { MODULES_GENERIC_APP_ID, MODULES_GENERIC_APP_NAME }; }
|
||||
|
||||
Application::Application(
|
||||
const WindowIdentity& identity,
|
||||
void* backendContext,
|
||||
Vec2F initialSize,
|
||||
std::filesystem::path resourceDirectory,
|
||||
DebugInterface debugInterface
|
||||
) :
|
||||
mWindow(std::make_unique<Window>(initialSize, identity, backendContext)) {
|
||||
if (resourceDirectory.empty()) {
|
||||
resourceDirectory = defaultResourceDirectory();
|
||||
}
|
||||
|
||||
mCanvas = std::make_unique<Canvas>(mWindow.get(), resourceDirectory);
|
||||
if (debugInterface == DebugInterface::Enabled) {
|
||||
mDebugGUI = std::make_unique<DebugGUI>(mWindow.get(), resourceDirectory);
|
||||
}
|
||||
|
||||
mPerSecondTimer.setDuration(1000.f);
|
||||
}
|
||||
|
||||
Application::~Application() {
|
||||
if (mRunning) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
void Application::start() {
|
||||
if (mRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
mHangWatchdog = std::make_unique<HangWatchdog>(hangSettings());
|
||||
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();
|
||||
|
||||
while (mRunning && !mWindow->controls().shouldClose()) {
|
||||
enterPhase(UiPhase::Idle);
|
||||
mWindow->loop().processEvents(!wantsFrame() || mWindow->loop().isWaitingForFrame());
|
||||
while (mWindow->loop().isWaitingForFrame() && !mWindow->controls().shouldClose()) {
|
||||
mWindow->loop().processEvents(true);
|
||||
}
|
||||
runFrame();
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
void Application::addHangReportFileSource(HangWatchdog::ReportFileSource reportFileSource) {
|
||||
if (mHangWatchdog) {
|
||||
mHangWatchdog->addReportFileSource(reportFileSource);
|
||||
}
|
||||
mHangReportFileSources.push_back(std::move(reportFileSource));
|
||||
}
|
||||
|
||||
HangSettings Application::hangSettings() const {
|
||||
HangSettings settings;
|
||||
settings.application = HangWatchdog::processName();
|
||||
return settings;
|
||||
}
|
||||
|
||||
void Application::enterPhase(UiPhase phase) {
|
||||
if (mHangWatchdog) {
|
||||
mHangWatchdog->enterPhase(phase);
|
||||
}
|
||||
}
|
||||
|
||||
bool Application::wantsFrame() { return forceNewFrame() || 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) {
|
||||
mFrameStart = get_time_ns();
|
||||
Profiler::global().beginFrame();
|
||||
|
||||
{
|
||||
enterPhase(UiPhase::Input);
|
||||
TP_PROFILE_SCOPE("frame.process");
|
||||
processPendingInput(deltaMs);
|
||||
}
|
||||
enterPhase(UiPhase::Draw);
|
||||
present();
|
||||
updateFrameCounters();
|
||||
|
||||
Profiler::global().endFrame();
|
||||
}
|
||||
|
||||
void Application::processPendingInput(halnf deltaMs) {
|
||||
InputState& inputState = mWindow->input().state();
|
||||
mWindow->input().flushPendingInput();
|
||||
|
||||
const size_t eventCount = inputState.pendingEventCount();
|
||||
TP_PROFILE_COUNT("input.events", eventCount);
|
||||
if (mHangWatchdog) {
|
||||
for (size_t index = 0; index < eventCount; index++) {
|
||||
mHangWatchdog->noteInputEvent();
|
||||
}
|
||||
}
|
||||
|
||||
inputState.applyPendingEvents();
|
||||
processFrame(&inputState, deltaMs);
|
||||
mFramesProcessed++;
|
||||
}
|
||||
|
||||
void Application::present() {
|
||||
TP_PROFILE_SCOPE("frame.present");
|
||||
if (!mWindow->loop().frameBegin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mCanvas->drawBegin();
|
||||
if (mDebugGUI) {
|
||||
mDebugGUI->drawBegin();
|
||||
}
|
||||
|
||||
drawFrameContent();
|
||||
|
||||
enterPhase(UiPhase::Present);
|
||||
mCanvas->drawEnd();
|
||||
if (mDebugGUI) {
|
||||
mDebugGUI->drawEnd();
|
||||
}
|
||||
|
||||
mWindow->loop().frameEnd();
|
||||
mFramesDrawn++;
|
||||
}
|
||||
|
||||
void Application::drawFrameContent() {
|
||||
TP_PROFILE_SCOPE("frame.draw");
|
||||
paintWindowBase();
|
||||
drawFrame(mCanvas.get());
|
||||
drawWindowDecorations();
|
||||
}
|
||||
|
||||
void Application::paintWindowBase() {
|
||||
const Canvas::SavedState saved(*mCanvas);
|
||||
mCanvas->setOrigin({ 0, 0 });
|
||||
mCanvas->rect({ { 0, 0 }, mWindow->frame().contentSize() }, kWindowBaseColor);
|
||||
}
|
||||
|
||||
void Application::drawWindowDecorations() {
|
||||
const WindowFrame& frame = mWindow->frame();
|
||||
if (!frame.hasShadow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Canvas::SavedState saved(*mCanvas);
|
||||
mCanvas->setOrigin({ 0, 0 });
|
||||
|
||||
cutWindowCorners(frame);
|
||||
drawWindowOutline(frame);
|
||||
drawWindowShadow(frame);
|
||||
}
|
||||
|
||||
void Application::cutWindowCorners(const WindowFrame& frame) {
|
||||
const RectF content = { { 0, 0 }, frame.contentSize() };
|
||||
|
||||
mCanvas->setCompositeOperation(Canvas::CompositeOperation::DestinationOut);
|
||||
mCanvas->beginPath();
|
||||
mCanvas->addPathRect(content);
|
||||
mCanvas->addPathRect(content, frame.cornerRadius());
|
||||
mCanvas->setPathWinding(Canvas::PathWinding::Hole);
|
||||
mCanvas->fillPath(RGBA(0, 0, 0, 1));
|
||||
}
|
||||
|
||||
void Application::drawWindowOutline(const WindowFrame& frame) {
|
||||
const RectF content = { { 0, 0 }, frame.contentSize() };
|
||||
|
||||
mCanvas->setCompositeOperation(Canvas::CompositeOperation::SourceOver);
|
||||
mCanvas->outline(content, kWindowOutlineColor, frame.cornerRadius(), kOutlineWidth);
|
||||
}
|
||||
|
||||
void Application::drawWindowShadow(const WindowFrame& frame) {
|
||||
const halnf margin = frame.margin();
|
||||
const bool focused = mWindow->controls().isFocused();
|
||||
const Canvas::BoxGradient shadow = {
|
||||
.box = RectF(Vec2F(0.0f, kShadowOffsetY), frame.contentSize()),
|
||||
.radius = frame.cornerRadius(),
|
||||
.feather = margin * kShadowFeatherFraction,
|
||||
.innerColor = focused ? kFocusedShadowColor : kUnfocusedShadowColor,
|
||||
.outerColor = kTransparent,
|
||||
};
|
||||
|
||||
mCanvas->setCompositeOperation(Canvas::CompositeOperation::DestinationOver);
|
||||
mCanvas->beginPath();
|
||||
mCanvas->addPathRect({ { -margin, -margin }, frame.surfaceSize() });
|
||||
mCanvas->fillPath(shadow);
|
||||
}
|
||||
|
||||
void Application::updateFrameCounters() {
|
||||
if (!mPerSecondTimer.isTimeout()) {
|
||||
return;
|
||||
}
|
||||
mPerSecondTimer.reset();
|
||||
|
||||
mFramesProcessedPerSecond = mFramesProcessed;
|
||||
mFramesDrawnPerSecond = mFramesDrawn;
|
||||
mFramesDrawn = 0;
|
||||
mFramesProcessed = 0;
|
||||
}
|
||||
|
||||
void Application::processFrame(InputState* inputState, halnf deltaTime) {}
|
||||
|
||||
void Application::drawFrame(Canvas* canvas) {}
|
||||
|
||||
void Application::drawDebug() {
|
||||
if (!mDebugGUI) {
|
||||
return;
|
||||
}
|
||||
|
||||
mDebugGUI->showValue("Frames processed per second", mFramesProcessedPerSecond);
|
||||
mDebugGUI->showValue("Frames drawn per second", mFramesDrawnPerSecond);
|
||||
}
|
||||
154
GraphicApplication/src/application/private/SessionRecording.cpp
Normal file
154
GraphicApplication/src/application/private/SessionRecording.cpp
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
#include "SessionRecording.hpp"
|
||||
|
||||
#include "Profiler.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
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(SessionRecorderHost& host) :
|
||||
mHost(host) {}
|
||||
|
||||
SessionRecorder::~SessionRecorder() { cancel(); }
|
||||
|
||||
double SessionRecorder::elapsedMs() const { return mRecording ? mHost.nowMs() - mRecording->startMs : 0.0; }
|
||||
|
||||
void SessionRecorder::start() {
|
||||
if (mRecording) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SessionEnvironment environment = mHost.environment();
|
||||
SessionRecording::Saver snapshot = mSnapshot ? mSnapshot(environment) : SessionRecording::Saver();
|
||||
mRecording = std::make_unique<Recording>([this]() { return mHost.nowMs(); });
|
||||
mRecording->startMs = mHost.nowMs();
|
||||
mRecording->environment = environment;
|
||||
mRecording->snapshot = std::move(snapshot);
|
||||
mRecording->input.setWindowSize(environment.windowSize);
|
||||
mRecording->input.startClockNow();
|
||||
|
||||
Profiler& profiler = Profiler::global();
|
||||
mRecording->profilerWasEnabled = profiler.isEnabled();
|
||||
profiler.clear();
|
||||
profiler.setEnabled(true);
|
||||
mHost.setRawInputTap(&mRecording->input);
|
||||
}
|
||||
|
||||
SessionRecording SessionRecorder::stop() {
|
||||
SessionRecording session;
|
||||
if (!mRecording) {
|
||||
return session;
|
||||
}
|
||||
|
||||
mHost.setRawInputTap(nullptr);
|
||||
session.environment = std::move(mRecording->environment);
|
||||
session.input = mRecording->input.recording();
|
||||
session.recordedFrames = recordedFramesTsv();
|
||||
session.saveSnapshot = std::move(mRecording->snapshot);
|
||||
|
||||
Profiler& profiler = Profiler::global();
|
||||
profiler.setEnabled(mRecording->profilerWasEnabled);
|
||||
profiler.clear();
|
||||
mRecording.reset();
|
||||
return session;
|
||||
}
|
||||
|
||||
void SessionRecorder::cancel() {
|
||||
if (!mRecording) {
|
||||
return;
|
||||
}
|
||||
mHost.setRawInputTap(nullptr);
|
||||
Profiler::global().setEnabled(mRecording->profilerWasEnabled);
|
||||
Profiler::global().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 = Profiler::global();
|
||||
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,83 @@
|
|||
#pragma once
|
||||
|
||||
#include "Canvas.hpp"
|
||||
#include "DebugGUI.hpp"
|
||||
#include "HangWatchdog.hpp"
|
||||
#include "Timing.hpp"
|
||||
#include "Window.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
enum class DebugInterface { Enabled, Disabled };
|
||||
|
||||
class Application {
|
||||
public:
|
||||
explicit Application(
|
||||
const WindowIdentity& identity = WindowIdentity::generic(),
|
||||
void* backendContext = nullptr,
|
||||
Vec2F initialSize = { 1500, 900 },
|
||||
std::filesystem::path resourceDirectory = {},
|
||||
DebugInterface debugInterface = DebugInterface::Enabled
|
||||
);
|
||||
virtual ~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.get(); }
|
||||
|
||||
virtual bool forceNewFrame() { return false; }
|
||||
virtual void processFrame(InputState* inputState, halnf deltaTime);
|
||||
virtual void drawFrame(Canvas* canvas);
|
||||
|
||||
void drawDebug();
|
||||
void addHangReportFileSource(HangWatchdog::ReportFileSource reportFileSource);
|
||||
[[nodiscard]] HangWatchdog* hangWatchdog() { return mHangWatchdog.get(); }
|
||||
|
||||
protected:
|
||||
[[nodiscard]] virtual HangSettings hangSettings() const;
|
||||
[[nodiscard]] halnf framesProcessedPerSecond() const { return mFramesProcessedPerSecond; }
|
||||
[[nodiscard]] halnf framesDrawnPerSecond() const { return mFramesDrawnPerSecond; }
|
||||
|
||||
private:
|
||||
void enterPhase(UiPhase phase);
|
||||
void runFrame();
|
||||
[[nodiscard]] bool wantsFrame();
|
||||
void runFrame(halnf deltaMs);
|
||||
void processPendingInput(halnf deltaMs);
|
||||
void present();
|
||||
void drawFrameContent();
|
||||
void paintWindowBase();
|
||||
void drawWindowDecorations();
|
||||
void cutWindowCorners(const WindowFrame& frame);
|
||||
void drawWindowOutline(const WindowFrame& frame);
|
||||
void drawWindowShadow(const WindowFrame& frame);
|
||||
void updateFrameCounters();
|
||||
|
||||
bool mRunning = false;
|
||||
time_ms mPreviousFrameTime = 0;
|
||||
time_ns mFrameStart = 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;
|
||||
|
||||
std::unique_ptr<Window> mWindow;
|
||||
std::unique_ptr<Canvas> mCanvas;
|
||||
std::unique_ptr<DebugGUI> mDebugGUI;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
#pragma once
|
||||
|
||||
#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;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
class SessionRecorderHost {
|
||||
public:
|
||||
virtual ~SessionRecorderHost() = default;
|
||||
|
||||
virtual void setRawInputTap(InputEventSink* tap) = 0;
|
||||
[[nodiscard]] virtual SessionEnvironment environment() const = 0;
|
||||
[[nodiscard]] virtual double nowMs() const = 0;
|
||||
};
|
||||
|
||||
class SessionRecorder {
|
||||
public:
|
||||
using Snapshot = std::function<SessionRecording::Saver(const SessionEnvironment& environment)>;
|
||||
|
||||
explicit SessionRecorder(SessionRecorderHost& host);
|
||||
~SessionRecorder();
|
||||
|
||||
SessionRecorder(const SessionRecorder&) = delete;
|
||||
SessionRecorder& operator=(const SessionRecorder&) = delete;
|
||||
|
||||
void setSnapshot(Snapshot snapshot) { mSnapshot = std::move(snapshot); }
|
||||
|
||||
[[nodiscard]] bool isRecording() const { return mRecording != nullptr; }
|
||||
[[nodiscard]] double elapsedMs() const;
|
||||
|
||||
void start();
|
||||
[[nodiscard]] SessionRecording stop();
|
||||
void cancel();
|
||||
|
||||
[[nodiscard]] static std::string defaultDirectory(const std::string& applicationSessionsDirectory);
|
||||
[[nodiscard]] static std::string recordedFramesTsv();
|
||||
|
||||
private:
|
||||
struct Recording;
|
||||
|
||||
SessionRecorderHost& mHost;
|
||||
Snapshot mSnapshot;
|
||||
std::unique_ptr<Recording> mRecording;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
#include "AndroidEntry.hpp"
|
||||
|
||||
#include "AndroidPlatform.hpp"
|
||||
|
||||
#include <android/log.h>
|
||||
#include <android_native_app_glue.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
|
||||
namespace tp {
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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, InputID>& keyCodes() {
|
||||
static const std::unordered_map<int32_t, InputID> keys = {
|
||||
{ AKEYCODE_SPACE, InputID::SPACE },
|
||||
{ AKEYCODE_APOSTROPHE, InputID::APOSTROPHE },
|
||||
{ AKEYCODE_COMMA, InputID::COMMA },
|
||||
{ AKEYCODE_MINUS, InputID::MINUS },
|
||||
{ AKEYCODE_PERIOD, InputID::PERIOD },
|
||||
{ AKEYCODE_SLASH, InputID::SLASH },
|
||||
{ AKEYCODE_0, InputID::N0 },
|
||||
{ AKEYCODE_1, InputID::N1 },
|
||||
{ AKEYCODE_2, InputID::N2 },
|
||||
{ AKEYCODE_3, InputID::N3 },
|
||||
{ AKEYCODE_4, InputID::N4 },
|
||||
{ AKEYCODE_5, InputID::N5 },
|
||||
{ AKEYCODE_6, InputID::N6 },
|
||||
{ AKEYCODE_7, InputID::N7 },
|
||||
{ AKEYCODE_8, InputID::N8 },
|
||||
{ AKEYCODE_9, InputID::N9 },
|
||||
{ AKEYCODE_SEMICOLON, InputID::SEMICOLON },
|
||||
{ AKEYCODE_EQUALS, InputID::EQUAL },
|
||||
{ AKEYCODE_A, InputID::A },
|
||||
{ AKEYCODE_B, InputID::B },
|
||||
{ AKEYCODE_C, InputID::C },
|
||||
{ AKEYCODE_D, InputID::D },
|
||||
{ AKEYCODE_E, InputID::E },
|
||||
{ AKEYCODE_F, InputID::F },
|
||||
{ AKEYCODE_G, InputID::G },
|
||||
{ AKEYCODE_H, InputID::H },
|
||||
{ AKEYCODE_I, InputID::I },
|
||||
{ AKEYCODE_J, InputID::J },
|
||||
{ AKEYCODE_K, InputID::K },
|
||||
{ AKEYCODE_L, InputID::L },
|
||||
{ AKEYCODE_M, InputID::M },
|
||||
{ AKEYCODE_N, InputID::N },
|
||||
{ AKEYCODE_O, InputID::O },
|
||||
{ AKEYCODE_P, InputID::P },
|
||||
{ AKEYCODE_Q, InputID::Q },
|
||||
{ AKEYCODE_R, InputID::R },
|
||||
{ AKEYCODE_S, InputID::S },
|
||||
{ AKEYCODE_T, InputID::T },
|
||||
{ AKEYCODE_U, InputID::U },
|
||||
{ AKEYCODE_V, InputID::V },
|
||||
{ AKEYCODE_W, InputID::W },
|
||||
{ AKEYCODE_X, InputID::X },
|
||||
{ AKEYCODE_Y, InputID::Y },
|
||||
{ AKEYCODE_Z, InputID::Z },
|
||||
{ AKEYCODE_LEFT_BRACKET, InputID::LEFT_BRACKET },
|
||||
{ AKEYCODE_BACKSLASH, InputID::BACKSLASH },
|
||||
{ AKEYCODE_RIGHT_BRACKET, InputID::RIGHT_BRACKET },
|
||||
{ AKEYCODE_GRAVE, InputID::GRAVE_ACCENT },
|
||||
{ AKEYCODE_ESCAPE, InputID::ESCAPE },
|
||||
{ AKEYCODE_ENTER, InputID::ENTER },
|
||||
{ AKEYCODE_TAB, InputID::TAB },
|
||||
{ AKEYCODE_DEL, InputID::BACKSPACE },
|
||||
{ AKEYCODE_INSERT, InputID::INSERT },
|
||||
{ AKEYCODE_FORWARD_DEL, InputID::DELETE_KEY },
|
||||
{ AKEYCODE_DPAD_RIGHT, InputID::RIGHT },
|
||||
{ AKEYCODE_DPAD_LEFT, InputID::LEFT },
|
||||
{ AKEYCODE_DPAD_DOWN, InputID::DOWN },
|
||||
{ AKEYCODE_DPAD_UP, InputID::UP },
|
||||
{ AKEYCODE_PAGE_UP, InputID::PAGE_UP },
|
||||
{ AKEYCODE_PAGE_DOWN, InputID::PAGE_DOWN },
|
||||
{ AKEYCODE_MOVE_HOME, InputID::HOME },
|
||||
{ AKEYCODE_MOVE_END, InputID::END },
|
||||
{ AKEYCODE_CAPS_LOCK, InputID::CAPS_LOCK },
|
||||
{ AKEYCODE_SCROLL_LOCK, InputID::SCROLL_LOCK },
|
||||
{ AKEYCODE_NUM_LOCK, InputID::NUM_LOCK },
|
||||
{ AKEYCODE_SYSRQ, InputID::PRINT_SCREEN },
|
||||
{ AKEYCODE_BREAK, InputID::PAUSE },
|
||||
{ AKEYCODE_F1, InputID::F1 },
|
||||
{ AKEYCODE_F2, InputID::F2 },
|
||||
{ AKEYCODE_F3, InputID::F3 },
|
||||
{ AKEYCODE_F4, InputID::F4 },
|
||||
{ AKEYCODE_F5, InputID::F5 },
|
||||
{ AKEYCODE_F6, InputID::F6 },
|
||||
{ AKEYCODE_F7, InputID::F7 },
|
||||
{ AKEYCODE_F8, InputID::F8 },
|
||||
{ AKEYCODE_F9, InputID::F9 },
|
||||
{ AKEYCODE_F10, InputID::F10 },
|
||||
{ AKEYCODE_F11, InputID::F11 },
|
||||
{ AKEYCODE_F12, InputID::F12 },
|
||||
{ AKEYCODE_NUMPAD_0, InputID::KP_0 },
|
||||
{ AKEYCODE_NUMPAD_1, InputID::KP_1 },
|
||||
{ AKEYCODE_NUMPAD_2, InputID::KP_2 },
|
||||
{ AKEYCODE_NUMPAD_3, InputID::KP_3 },
|
||||
{ AKEYCODE_NUMPAD_4, InputID::KP_4 },
|
||||
{ AKEYCODE_NUMPAD_5, InputID::KP_5 },
|
||||
{ AKEYCODE_NUMPAD_6, InputID::KP_6 },
|
||||
{ AKEYCODE_NUMPAD_7, InputID::KP_7 },
|
||||
{ AKEYCODE_NUMPAD_8, InputID::KP_8 },
|
||||
{ AKEYCODE_NUMPAD_9, InputID::KP_9 },
|
||||
{ AKEYCODE_NUMPAD_DOT, InputID::KP_DECIMAL },
|
||||
{ AKEYCODE_NUMPAD_DIVIDE, InputID::KP_DIVIDE },
|
||||
{ AKEYCODE_NUMPAD_MULTIPLY, InputID::KP_MULTIPLY },
|
||||
{ AKEYCODE_NUMPAD_SUBTRACT, InputID::KP_SUBTRACT },
|
||||
{ AKEYCODE_NUMPAD_ADD, InputID::KP_ADD },
|
||||
{ AKEYCODE_NUMPAD_ENTER, InputID::KP_ENTER },
|
||||
{ AKEYCODE_NUMPAD_EQUALS, InputID::KP_EQUAL },
|
||||
{ AKEYCODE_SHIFT_LEFT, InputID::LEFT_SHIFT },
|
||||
{ AKEYCODE_CTRL_LEFT, InputID::LEFT_CONTROL },
|
||||
{ AKEYCODE_ALT_LEFT, InputID::LEFT_ALT },
|
||||
{ AKEYCODE_META_LEFT, InputID::LEFT_SUPER },
|
||||
{ AKEYCODE_SHIFT_RIGHT, InputID::RIGHT_SHIFT },
|
||||
{ AKEYCODE_CTRL_RIGHT, InputID::RIGHT_CONTROL },
|
||||
{ AKEYCODE_ALT_RIGHT, InputID::RIGHT_ALT },
|
||||
{ AKEYCODE_META_RIGHT, InputID::RIGHT_SUPER },
|
||||
{ AKEYCODE_MENU, InputID::MENU },
|
||||
};
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<InputID> inputFromKeyCode(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<InputID> inputFromKeyCode(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,521 @@
|
|||
#include "AndroidWindow.hpp"
|
||||
|
||||
#include "AndroidKeys.hpp"
|
||||
#include "AndroidPlatform.hpp"
|
||||
#include "VulkanContext.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 <optional>
|
||||
#include <cmath>
|
||||
|
||||
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<InputID> buttonFor(int32_t androidButton) {
|
||||
switch (androidButton) {
|
||||
case AMOTION_EVENT_BUTTON_PRIMARY: return InputID::MOUSE1;
|
||||
case AMOTION_EVENT_BUTTON_SECONDARY: return InputID::MOUSE2;
|
||||
case AMOTION_EVENT_BUTTON_TERTIARY: return InputID::MOUSE3;
|
||||
case AMOTION_EVENT_BUTTON_BACK: return InputID::MOUSE4;
|
||||
case AMOTION_EVENT_BUTTON_FORWARD: return InputID::MOUSE5;
|
||||
default: return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
class MutedInput : public InputEventSink {};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
std::vector<const char*> AndroidWindow::requiredInstanceExtensions() const {
|
||||
return { VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME };
|
||||
}
|
||||
|
||||
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();
|
||||
if (VulkanContext* vulkan = mHost.vulkan(); vulkan && !vulkan->hasSurface()) {
|
||||
vulkan->recreateSurface();
|
||||
}
|
||||
__android_log_print(
|
||||
ANDROID_LOG_INFO,
|
||||
kTag,
|
||||
"window %dx%d scale %.2f",
|
||||
ANativeWindow_getWidth(window),
|
||||
ANativeWindow_getHeight(window),
|
||||
mScale
|
||||
);
|
||||
}
|
||||
|
||||
void AndroidWindow::detachWindow() {
|
||||
if (VulkanContext* vulkan = mHost.vulkan()) {
|
||||
vulkan->releaseSurface();
|
||||
}
|
||||
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;
|
||||
platformInput().onKeyboardFocus(true);
|
||||
break;
|
||||
case APP_CMD_LOST_FOCUS:
|
||||
mFocused = false;
|
||||
platformInput().onKeyboardFocus(false);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
InputEventSink& AndroidWindow::platformInput() {
|
||||
static MutedInput muted;
|
||||
if (!mAcceptsInput) {
|
||||
return muted;
|
||||
}
|
||||
return mCoalescer.recorder();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
InputEventSink& input = platformInput();
|
||||
|
||||
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.onPointerSample(sample(SIZE_MAX));
|
||||
input.onPointerMoved(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.onPointerButton(InputID::MOUSE1, 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.onPointerSample(sample(index));
|
||||
input.onPointerMoved(position(index));
|
||||
}
|
||||
input.onPointerSample(sample(SIZE_MAX));
|
||||
input.onPointerMoved(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.onPointerSample(released);
|
||||
input.onPointerMoved(position(SIZE_MAX));
|
||||
if (action == AMOTION_EVENT_ACTION_BUTTON_RELEASE) {
|
||||
deliverButtonChanges(input, AMotionEvent_getButtonState(event));
|
||||
} else if (mPrimaryHeld) {
|
||||
mPrimaryHeld = false;
|
||||
input.onPointerButton(InputID::MOUSE1, false);
|
||||
}
|
||||
if (toolType == AMOTION_EVENT_TOOL_TYPE_FINGER) {
|
||||
input.onPointerLeft();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case AMOTION_EVENT_ACTION_HOVER_EXIT: input.onPointerLeft(); 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.onPointerMoved(position(SIZE_MAX));
|
||||
input.onScrolled(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;
|
||||
}
|
||||
|
||||
InputEventSink& input = platformInput();
|
||||
std::vector<TouchGesture> gestures;
|
||||
mGestures.update(touches, gestures, (uint32_t) (AMotionEvent_getEventTime(event) / 1000000));
|
||||
for (const TouchGesture& gesture : gestures) {
|
||||
input.onGesture(gesture);
|
||||
}
|
||||
|
||||
if (touches.size() >= 2 && !mGestureHoldsPointer) {
|
||||
mGestureHoldsPointer = true;
|
||||
if (mPrimaryHeld) {
|
||||
mPrimaryHeld = false;
|
||||
input.onPointerButton(InputID::MOUSE1, false);
|
||||
}
|
||||
input.onPointerLeft();
|
||||
}
|
||||
|
||||
if (ending) {
|
||||
mGestureHoldsPointer = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AndroidWindow::deliverButtonChanges(InputEventSink& 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.onPointerButton(*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);
|
||||
InputEventSink& input = platformInput();
|
||||
const auto id = keyCode == AKEYCODE_BACK ? std::optional(InputID::ESCAPE) : android::inputFromKeyCode(keyCode);
|
||||
if (id) {
|
||||
if (action == AKEY_EVENT_ACTION_DOWN) {
|
||||
if (AKeyEvent_getRepeatCount(event) > 0) {
|
||||
input.onKeyRepeat(*id);
|
||||
} else {
|
||||
input.onKey(*id, true);
|
||||
}
|
||||
} else if (action == AKEY_EVENT_ACTION_UP) {
|
||||
input.onKey(*id, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (action == AKEY_EVENT_ACTION_DOWN) {
|
||||
const std::string text = android::unicodeText(mApp, event);
|
||||
if (!text.empty()) {
|
||||
input.onText(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());
|
||||
}
|
||||
|
||||
void AndroidWindow::flushPendingInput() { mCoalescer.deliver(mHost.inputSink()); }
|
||||
|
||||
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;
|
||||
info.hasWindowControls = false;
|
||||
info.canMoveWindow = false;
|
||||
info.canResizeWindow = false;
|
||||
return info;
|
||||
}
|
||||
|
||||
std::unique_ptr<PlatformWindow>
|
||||
createPlatformWindow(PlatformWindowHost& host, Vec2F, const WindowIdentity&, void* backendContext) {
|
||||
auto* app = static_cast<android_app*>(backendContext);
|
||||
if (!app) {
|
||||
throw std::runtime_error("Android backend requires an android_app context");
|
||||
}
|
||||
return std::make_unique<AndroidWindow>(host, *app);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
#pragma once
|
||||
|
||||
#include "PlatformWindow.hpp"
|
||||
#include "TouchGesture.hpp"
|
||||
#include "InputEventSink.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]] std::vector<const char*> requiredInstanceExtensions() const override;
|
||||
[[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 accepts) override { mAcceptsInput = accepts; }
|
||||
|
||||
void dispatchEvents(halnf timeoutMs) override;
|
||||
void wake() override;
|
||||
void waitUntilConfigured() override;
|
||||
void commitFrameGeometry() override {}
|
||||
void flushPendingInput() 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]] Vec2F contentSize() const override { return mFrame.contentSize(); }
|
||||
[[nodiscard]] halnf scale() const override { return mScale; }
|
||||
[[nodiscard]] halnf refreshIntervalMs() const override { return 1000.0f / 60.0f; }
|
||||
[[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;
|
||||
|
||||
[[nodiscard]] InputCoalescer& coalescer() override { return mCoalescer; }
|
||||
[[nodiscard]] const InputCoalescer& coalescer() const override { return mCoalescer; }
|
||||
|
||||
[[nodiscard]] bool hasSurface() const { return mNativeWindow != nullptr; }
|
||||
|
||||
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(InputEventSink& 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]] InputEventSink& platformInput();
|
||||
|
||||
PlatformWindowHost& mHost;
|
||||
android_app& mApp;
|
||||
ANativeWindow* mNativeWindow = nullptr;
|
||||
WindowFrame mFrame;
|
||||
InputCoalescer mCoalescer;
|
||||
std::string mClipboard;
|
||||
halnf mScale = 1;
|
||||
bool mAcceptsInput = true;
|
||||
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,12 @@
|
|||
#pragma once
|
||||
|
||||
struct android_app;
|
||||
|
||||
namespace tp {
|
||||
|
||||
using AndroidMain = int (*)(android_app*);
|
||||
|
||||
void runAndroidApplication(
|
||||
android_app* app, AndroidMain main, const char* const* assetDirectories, int assetDirectoryCount
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
#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, window:%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" : "",
|
||||
info.hasWindowControls ? " controls" : "",
|
||||
info.canMoveWindow ? " move" : "",
|
||||
info.canResizeWindow ? " resize" : ""
|
||||
);
|
||||
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,50 @@
|
|||
#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 hasWindowControls = false;
|
||||
bool canMoveWindow = false;
|
||||
bool canResizeWindow = 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,71 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputCoalescer.hpp"
|
||||
#include "PlatformInfo.hpp"
|
||||
#include "Rect.hpp"
|
||||
#include "VulkanContext.hpp"
|
||||
#include "WindowFrame.hpp"
|
||||
#include "WindowTypes.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace tp {
|
||||
|
||||
class InputEventSink;
|
||||
class PlatformWindowDesktopControls;
|
||||
|
||||
class PlatformWindowHost {
|
||||
public:
|
||||
virtual ~PlatformWindowHost() = default;
|
||||
|
||||
[[nodiscard]] virtual InputEventSink& inputSink() = 0;
|
||||
virtual void onContentResized() = 0;
|
||||
[[nodiscard]] virtual VulkanContext* vulkan() = 0;
|
||||
};
|
||||
|
||||
class PlatformWindow : public VulkanSurfaceSource {
|
||||
public:
|
||||
~PlatformWindow() override = default;
|
||||
|
||||
virtual void setAcceptsInput(bool accepts) = 0;
|
||||
|
||||
virtual void dispatchEvents(halnf timeoutMs) = 0;
|
||||
virtual void wake() = 0;
|
||||
virtual void waitUntilConfigured() = 0;
|
||||
virtual void commitFrameGeometry() = 0;
|
||||
virtual void flushPendingInput() = 0;
|
||||
[[nodiscard]] virtual bool isWaitingForFrame() const = 0;
|
||||
|
||||
[[nodiscard]] virtual bool shouldClose() const = 0;
|
||||
virtual void requestClose() = 0;
|
||||
|
||||
[[nodiscard]] virtual const WindowFrame& frame() const = 0;
|
||||
[[nodiscard]] virtual Vec2F contentSize() const = 0;
|
||||
[[nodiscard]] virtual halnf scale() const = 0;
|
||||
[[nodiscard]] virtual halnf refreshIntervalMs() const = 0;
|
||||
[[nodiscard]] virtual bool isMaximized() const = 0;
|
||||
[[nodiscard]] virtual bool isFocused() const = 0;
|
||||
|
||||
virtual void minimize() = 0;
|
||||
[[nodiscard]] virtual bool isFullScreen() const = 0;
|
||||
[[nodiscard]] virtual PlatformWindowDesktopControls* 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]] virtual InputCoalescer& coalescer() = 0;
|
||||
[[nodiscard]] virtual const InputCoalescer& coalescer() const = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::unique_ptr<PlatformWindow> createPlatformWindow(
|
||||
PlatformWindowHost& host, Vec2F contentSize, const WindowIdentity& identity, void* backendContext
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
#pragma once
|
||||
|
||||
#include "Vec.hpp"
|
||||
#include "WindowTypes.hpp"
|
||||
|
||||
namespace tp {
|
||||
class PlatformWindowDesktopControls {
|
||||
public:
|
||||
virtual ~PlatformWindowDesktopControls() = 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,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,220 @@
|
|||
#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); }
|
||||
|
||||
static void
|
||||
outputGeometry(void*, wl_output*, int32_t, int32_t, int32_t, int32_t, int32_t, const char*, const char*, int32_t) {}
|
||||
|
||||
static void outputMode(void* data, wl_output*, uint32_t flags, int32_t, int32_t, int32_t refresh) {
|
||||
if (!(flags & WL_OUTPUT_MODE_CURRENT) || refresh <= 0) {
|
||||
return;
|
||||
}
|
||||
static_cast<WaylandConnection*>(data)->mRefreshMillihertz = refresh;
|
||||
}
|
||||
|
||||
static void outputDone(void*, wl_output*) {}
|
||||
static void outputScale(void*, wl_output*, int32_t) {}
|
||||
static void outputName(void*, wl_output*, const char*) {}
|
||||
static void outputDescription(void*, wl_output*, const char*) {}
|
||||
};
|
||||
|
||||
namespace {
|
||||
const wl_registry_listener kRegistryListener = {
|
||||
WaylandRegistryListener::global,
|
||||
WaylandRegistryListener::globalRemove,
|
||||
};
|
||||
|
||||
const xdg_wm_base_listener kWindowManagerListener = { WaylandRegistryListener::ping };
|
||||
|
||||
const wl_output_listener kOutputListener = {
|
||||
WaylandRegistryListener::outputGeometry, WaylandRegistryListener::outputMode,
|
||||
WaylandRegistryListener::outputDone, WaylandRegistryListener::outputScale,
|
||||
WaylandRegistryListener::outputName, WaylandRegistryListener::outputDescription,
|
||||
};
|
||||
|
||||
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 (mOutput) {
|
||||
wl_output_release(mOutput);
|
||||
}
|
||||
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);
|
||||
} else if (interface == wl_output_interface.name && !mOutput) {
|
||||
mOutput = bindAs<wl_output>(registry, name, &wl_output_interface, version, 4);
|
||||
wl_output_add_listener(mOutput, &kOutputListener, this);
|
||||
}
|
||||
}
|
||||
|
||||
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,76 @@
|
|||
#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 wl_output;
|
||||
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; }
|
||||
[[nodiscard]] int32_t refreshMillihertz() const { return mRefreshMillihertz; }
|
||||
|
||||
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;
|
||||
wl_output* mOutput = nullptr;
|
||||
int32_t mRefreshMillihertz = 60000;
|
||||
};
|
||||
}
|
||||
1281
GraphicApplication/src/backends/wayland/private/WaylandSeat.cpp
Normal file
1281
GraphicApplication/src/backends/wayland/private/WaylandSeat.cpp
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,72 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputCodes.hpp"
|
||||
#include "Rect.hpp"
|
||||
#include "WindowTypes.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
struct wl_surface;
|
||||
|
||||
namespace tp {
|
||||
|
||||
class WaylandConnection;
|
||||
class WaylandWindow;
|
||||
class InputEventSink;
|
||||
|
||||
class WaylandSeat {
|
||||
public:
|
||||
WaylandSeat(WaylandConnection& connection, WaylandWindow& window);
|
||||
~WaylandSeat();
|
||||
|
||||
WaylandSeat(const WaylandSeat&) = delete;
|
||||
WaylandSeat& operator=(const WaylandSeat&) = delete;
|
||||
|
||||
[[nodiscard]] int repeatTimerDescriptor() const;
|
||||
void onRepeatTimer();
|
||||
|
||||
[[nodiscard]] uint32_t lastInputSerial() const { return mLastInputSerial; }
|
||||
|
||||
void setCursor(CursorShape shape);
|
||||
|
||||
void setClipboardText(const std::string& text);
|
||||
[[nodiscard]] std::string clipboardText();
|
||||
[[nodiscard]] ClipboardImage clipboardImage();
|
||||
|
||||
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; }
|
||||
|
||||
class Keyboard;
|
||||
class Pointer;
|
||||
class Clipboard;
|
||||
class TextInput;
|
||||
class Tablet;
|
||||
|
||||
private:
|
||||
friend struct WaylandSeatListeners;
|
||||
|
||||
void onCapabilities(uint32_t capabilities);
|
||||
void recordSerial(uint32_t serial) { mLastInputSerial = serial; }
|
||||
|
||||
WaylandConnection& mConnection;
|
||||
WaylandWindow& mWindow;
|
||||
|
||||
[[nodiscard]] InputEventSink& input();
|
||||
|
||||
std::unique_ptr<Keyboard> mKeyboard;
|
||||
std::unique_ptr<Pointer> mPointer;
|
||||
std::unique_ptr<Clipboard> mClipboard;
|
||||
std::unique_ptr<TextInput> mTextInput;
|
||||
std::unique_ptr<Tablet> mTablet;
|
||||
|
||||
uint32_t mLastInputSerial = 0;
|
||||
bool mHasTouch = false;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,664 @@
|
|||
#include "WaylandWindow.hpp"
|
||||
|
||||
#include <sys/eventfd.h>
|
||||
|
||||
#include "KeyMapping.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 <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;
|
||||
WindowConfigure 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);
|
||||
WindowConfigure configure = self.mObjects->pending;
|
||||
configure.serial = serial;
|
||||
self.mCoalescer.configure(configure);
|
||||
}
|
||||
|
||||
static void toplevelConfigure(void* data, xdg_toplevel*, int32_t width, int32_t height, wl_array* states) {
|
||||
WindowConfigure configuration;
|
||||
configuration.width = width;
|
||||
configuration.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: configuration.maximized = true; break;
|
||||
case XDG_TOPLEVEL_STATE_FULLSCREEN: configuration.fullscreen = true; break;
|
||||
case XDG_TOPLEVEL_STATE_ACTIVATED: configuration.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: configuration.tiled = true; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
window(data).mObjects->pending = configuration;
|
||||
}
|
||||
|
||||
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, *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();
|
||||
}
|
||||
|
||||
std::vector<const char*> WaylandWindow::requiredInstanceExtensions() const {
|
||||
return { VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME };
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
halnf WaylandWindow::refreshIntervalMs() const { return 1.0e6f / (halnf) mConnection->refreshMillihertz(); }
|
||||
|
||||
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();
|
||||
|
||||
const int repeatTimer = mSeat->repeatTimerDescriptor();
|
||||
pollfd descriptors[3] = {
|
||||
{ wl_display_get_fd(display), POLLIN, 0 },
|
||||
{ mWakeDescriptor, POLLIN, 0 },
|
||||
{ repeatTimer, POLLIN, 0 },
|
||||
};
|
||||
const nfds_t descriptorCount = repeatTimer >= 0 ? 3 : 2;
|
||||
|
||||
const int ready = poll(descriptors, descriptorCount, 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 && descriptorCount == 3 && (descriptors[2].revents & POLLIN)) {
|
||||
mSeat->onRepeatTimer();
|
||||
handled = true;
|
||||
}
|
||||
|
||||
mConnection->flush();
|
||||
return handled;
|
||||
}
|
||||
|
||||
void WaylandWindow::waitUntilConfigured() {
|
||||
while (!mConfigured) {
|
||||
if (!mConnection->dispatch(kConfigureWaitMs)) {
|
||||
break;
|
||||
}
|
||||
flushPendingInput();
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandWindow::applyConfiguration(const WindowConfigure& configuration) {
|
||||
mMaximized = configuration.maximized || configuration.fullscreen;
|
||||
mFullScreen = configuration.fullscreen;
|
||||
mActivated = configuration.activated;
|
||||
|
||||
const bool tiled = mMaximized || configuration.tiled;
|
||||
if (tiled != mFrame.isTiled()) {
|
||||
mFrame.setTiled(tiled);
|
||||
mGeometryDirty = true;
|
||||
}
|
||||
|
||||
Vec2F contentSize = mFrame.contentSize();
|
||||
if (configuration.width > 0 && configuration.height > 0) {
|
||||
contentSize = { (halnf) configuration.width, (halnf) configuration.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(); }
|
||||
|
||||
namespace {
|
||||
class MutedInput : public InputEventSink {};
|
||||
}
|
||||
|
||||
InputEventSink& WaylandWindow::platformInput() {
|
||||
static MutedInput muted;
|
||||
if (!mAcceptsInput) {
|
||||
return muted;
|
||||
}
|
||||
return mCoalescer.recorder();
|
||||
}
|
||||
|
||||
void WaylandWindow::flushPendingInput() {
|
||||
if (mCoalescer.hasPendingConfigure()) {
|
||||
const WindowConfigure configure = mCoalescer.takeConfigure();
|
||||
applyConfiguration(configure);
|
||||
xdg_surface_ack_configure(mObjects->shellSurface, configure.serial);
|
||||
mConfigured = true;
|
||||
notifyOwnerResized();
|
||||
}
|
||||
|
||||
mCoalescer.deliver(mHost.inputSink());
|
||||
}
|
||||
|
||||
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) {
|
||||
const halnf outside = kPointerOutsideCoordinate;
|
||||
mSeat->setCursor(cursorForEdge(edge));
|
||||
platformInput().onPointerMoved({ outside, outside });
|
||||
return;
|
||||
}
|
||||
|
||||
mSeat->setCursor(mClientCursor);
|
||||
platformInput().onPointerMoved(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);
|
||||
platformInput().onGesture(content);
|
||||
}
|
||||
|
||||
void WaylandWindow::onPointerSurfaceLeft() {
|
||||
mHeldButtons = 0;
|
||||
mHoveredEdge = WindowEdge::None;
|
||||
platformInput().onPointerLeft();
|
||||
}
|
||||
|
||||
void WaylandWindow::onPointerSurfaceButton(uint32_t button, bool pressed, uint32_t serial) {
|
||||
if (pressed && mHoveredEdge != WindowEdge::None) {
|
||||
if (button == BTN_LEFT) {
|
||||
xdg_toplevel_resize(mObjects->toplevel, mConnection->seat(), serial, resizeEdgeCode(mHoveredEdge));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const std::optional<InputID> id = inputFromPointerButton(button);
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pressed) {
|
||||
mHeldButtons++;
|
||||
} else if (mHeldButtons > 0) {
|
||||
mHeldButtons--;
|
||||
}
|
||||
platformInput().onPointerButton(*id, 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.hasWindowControls = true;
|
||||
info.canMoveWindow = true;
|
||||
info.canResizeWindow = true;
|
||||
info.hasHoverPointer = info.hasMouse || info.hasPen;
|
||||
return info;
|
||||
}
|
||||
|
||||
std::unique_ptr<PlatformWindow>
|
||||
createPlatformWindow(PlatformWindowHost& host, Vec2F contentSize, const WindowIdentity& identity, void*) {
|
||||
return std::make_unique<WaylandWindow>(host, contentSize, identity);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
#pragma once
|
||||
|
||||
#include "TouchGesture.hpp"
|
||||
#include "PlatformWindow.hpp"
|
||||
#include "PlatformWindowDesktopControls.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
struct wl_display;
|
||||
|
||||
namespace tp {
|
||||
|
||||
class WaylandConnection;
|
||||
class WaylandSeat;
|
||||
class InputEventSink;
|
||||
|
||||
class WaylandWindow final : public PlatformWindow, public PlatformWindowDesktopControls {
|
||||
public:
|
||||
WaylandWindow(PlatformWindowHost& host, Vec2F contentSize, const WindowIdentity& identity);
|
||||
~WaylandWindow() override;
|
||||
|
||||
WaylandWindow(const WaylandWindow&) = delete;
|
||||
WaylandWindow& operator=(const WaylandWindow&) = delete;
|
||||
|
||||
[[nodiscard]] std::vector<const char*> requiredInstanceExtensions() const override;
|
||||
[[nodiscard]] VkSurfaceKHR createSurface(VkInstance instance) const override;
|
||||
[[nodiscard]] VkExtent2D framebufferExtent() const override;
|
||||
[[nodiscard]] bool wantsTranslucency() const override { return true; }
|
||||
|
||||
[[nodiscard]] InputEventSink& platformInput();
|
||||
void setAcceptsInput(bool accepts) override;
|
||||
void onTranslucencyResolved(bool translucent) override;
|
||||
|
||||
void dispatchEvents(halnf timeoutMs) override;
|
||||
void wake() override;
|
||||
void waitUntilConfigured() override;
|
||||
void commitFrameGeometry() override;
|
||||
|
||||
[[nodiscard]] bool shouldClose() const override { return mCloseRequested; }
|
||||
void requestClose() override { mCloseRequested = true; }
|
||||
|
||||
[[nodiscard]] const WindowFrame& frame() const override { return mFrame; }
|
||||
[[nodiscard]] Vec2F contentSize() const override { return mFrame.contentSize(); }
|
||||
[[nodiscard]] halnf scale() const override { return mScale; }
|
||||
[[nodiscard]] halnf refreshIntervalMs() const override;
|
||||
[[nodiscard]] bool isMaximized() const override { return mMaximized; }
|
||||
[[nodiscard]] bool isFocused() const override { return mActivated; }
|
||||
|
||||
void minimize() override;
|
||||
[[nodiscard]] PlatformWindowDesktopControls* 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;
|
||||
|
||||
[[nodiscard]] InputCoalescer& coalescer() override { return mCoalescer; }
|
||||
[[nodiscard]] const InputCoalescer& coalescer() const override { return mCoalescer; }
|
||||
[[nodiscard]] bool isWaitingForFrame() const override;
|
||||
void flushPendingInput() override;
|
||||
|
||||
private:
|
||||
friend class WaylandSeat;
|
||||
friend struct WaylandListeners;
|
||||
|
||||
void applyConfiguration(const WindowConfigure& configuration);
|
||||
void applyScale(halnf scale);
|
||||
void notifyOwnerResized();
|
||||
[[nodiscard]] bool pollOnce(int timeoutMs);
|
||||
void updateFrameGeometry();
|
||||
void requestFrameCallback();
|
||||
|
||||
void onPointerSurfaceMove(Vec2F surfacePoint);
|
||||
void onPointerSurfaceButton(uint32_t button, bool pressed, uint32_t serial);
|
||||
void onPointerSurfaceLeft();
|
||||
void onPointerSurfacePinch(const TouchGesture& gesture);
|
||||
[[nodiscard]] Vec2F toContent(Vec2F surfacePoint) const;
|
||||
[[nodiscard]] std::string activationToken();
|
||||
|
||||
PlatformWindowHost& mHost;
|
||||
bool mAcceptsInput = true;
|
||||
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;
|
||||
InputCoalescer mCoalescer;
|
||||
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;
|
||||
};
|
||||
}
|
||||
473
GraphicApplication/src/canvas/private/Canvas.cpp
Normal file
473
GraphicApplication/src/canvas/private/Canvas.cpp
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
#include "CanvasContext.hpp"
|
||||
|
||||
#include "Profiler.hpp"
|
||||
#include "VulkanContext.hpp"
|
||||
|
||||
#include <nanovg_vk.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
using namespace tp;
|
||||
|
||||
namespace {
|
||||
constexpr halnf kItalicSkew = -0.21f;
|
||||
|
||||
enum class HorizontalAlign { Center = 0x00, Left = 0x01, Right = 0x02 };
|
||||
enum class VerticalAlign { Center = 0x00, Top = 0x01, Bottom = 0x02 };
|
||||
|
||||
HorizontalAlign horizontalPartOf(Canvas::Align align) { return (HorizontalAlign) (((int) align >> 8) & 0xFF); }
|
||||
|
||||
VerticalAlign verticalPartOf(Canvas::Align align) { return (VerticalAlign) ((int) align & 0xFF); }
|
||||
|
||||
bool isItalic(Canvas::FontFace face) {
|
||||
return face == Canvas::FontFace::Italic || face == Canvas::FontFace::BoldItalic;
|
||||
}
|
||||
|
||||
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(Window* window, const std::filesystem::path& resourceDirectory) {
|
||||
mContext = new Context();
|
||||
mContext->window = window;
|
||||
|
||||
VulkanContext* vulkan = window->vulkan();
|
||||
|
||||
VKNVGCreateInfo createInfo{};
|
||||
createInfo.gpu = vulkan->physicalDevice();
|
||||
createInfo.device = vulkan->device();
|
||||
createInfo.renderpass = vulkan->renderPass();
|
||||
createInfo.cmdBuffer = vulkan->commandBufferArray();
|
||||
createInfo.graphicsQueueFamilyIndex = vulkan->graphicsQueueFamily();
|
||||
createInfo.currentFrame = vulkan->currentFrameSlot();
|
||||
createInfo.swapchainImageCount = vulkan->framesInFlight();
|
||||
|
||||
mContext->vg = nvgCreateVk(&createInfo, NVG_ANTIALIAS | NVG_STENCIL_STROKES, vulkan->graphicsQueue());
|
||||
mContext->fonts = loadCanvasFonts(mContext->vg, resourceDirectory);
|
||||
}
|
||||
|
||||
Canvas::~Canvas() {
|
||||
mContext->window->vulkan()->waitIdle();
|
||||
nvgDeleteVk(mContext->vg);
|
||||
delete mContext;
|
||||
}
|
||||
|
||||
void Canvas::drawBegin() {
|
||||
const WindowFrame& frame = mContext->window->frame();
|
||||
const Vec2F surfaceSize = frame.surfaceSize();
|
||||
const Vec2F framebufferSize = mContext->window->getFramebufferSize();
|
||||
const halnf devicePixelRatio = surfaceSize.x > 0 ? framebufferSize.x / surfaceSize.x : 1.0f;
|
||||
|
||||
nvgBeginFrame(mContext->vg, surfaceSize.x, surfaceSize.y, devicePixelRatio);
|
||||
nvgTranslate(mContext->vg, frame.margin(), frame.margin());
|
||||
}
|
||||
|
||||
void Canvas::drawEnd() {
|
||||
TP_PROFILE_SCOPE("frame.ui_flush");
|
||||
nvgEndFrame(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::save() {
|
||||
nvgSave(mContext->vg);
|
||||
mSavedFrames.append({ mOrigin, mScissors.size() });
|
||||
}
|
||||
|
||||
void Canvas::restore() {
|
||||
DEBUG_ASSERT(mSavedFrames.size());
|
||||
const SavedFrame saved = mSavedFrames.last();
|
||||
mSavedFrames.pop();
|
||||
|
||||
while (mScissors.size() > saved.clampDepth) {
|
||||
mScissors.pop();
|
||||
}
|
||||
mOrigin = saved.origin;
|
||||
nvgRestore(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::setCompositeOperation(CompositeOperation operation) {
|
||||
nvgGlobalCompositeOperation(mContext->vg, nvgCompositeOperationOf(operation));
|
||||
}
|
||||
|
||||
void Canvas::beginPath() { nvgBeginPath(mContext->vg); }
|
||||
|
||||
void Canvas::addPathRect(RectF rec, halnf round) {
|
||||
rec.pos += mOrigin;
|
||||
|
||||
if (round == 0) {
|
||||
nvgRect(mContext->vg, rec.x, rec.y, rec.z, rec.w);
|
||||
} else {
|
||||
nvgRoundedRect(mContext->vg, rec.x, rec.y, rec.z, rec.w, round);
|
||||
}
|
||||
}
|
||||
|
||||
void Canvas::setPathWinding(PathWinding winding) { nvgPathWinding(mContext->vg, nvgWindingOf(winding)); }
|
||||
|
||||
void Canvas::fillPath(const RGBA& col) {
|
||||
nvgFillColor(mContext->vg, toNVGColor(col));
|
||||
nvgFill(mContext->vg);
|
||||
}
|
||||
|
||||
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 rec, const RGBA& col, halnf round) {
|
||||
rec.pos += mOrigin;
|
||||
|
||||
nvgBeginPath(mContext->vg);
|
||||
|
||||
if (round == 0) {
|
||||
nvgRect(mContext->vg, rec.x, rec.y, rec.z, rec.w);
|
||||
} else {
|
||||
nvgRoundedRect(mContext->vg, rec.x, rec.y, rec.z, rec.w, round);
|
||||
}
|
||||
|
||||
nvgFillColor(mContext->vg, { col.r, col.g, col.b, col.a });
|
||||
nvgFill(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::gradientRect(RectF rec, const RGBA& from, const RGBA& to, bool horizontal, halnf round) {
|
||||
rec.pos += mOrigin;
|
||||
auto vg = mContext->vg;
|
||||
const halnf endX = horizontal ? rec.x + rec.z : rec.x;
|
||||
const halnf endY = horizontal ? rec.y : rec.y + rec.w;
|
||||
const NVGpaint paint =
|
||||
nvgLinearGradient(vg, rec.x, rec.y, endX, endY, { from.r, from.g, from.b, from.a }, { to.r, to.g, to.b, to.a });
|
||||
nvgBeginPath(vg);
|
||||
if (round == 0) {
|
||||
nvgRect(vg, rec.x, rec.y, rec.z, rec.w);
|
||||
} else {
|
||||
nvgRoundedRect(vg, rec.x, rec.y, rec.z, rec.w, round);
|
||||
}
|
||||
nvgFillPaint(vg, paint);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
void Canvas::debugCross(RectF rec, const RGBA& col) {
|
||||
nvgBeginPath(mContext->vg);
|
||||
|
||||
nvgMoveTo(mContext->vg, rec.p1().x, rec.p1().y);
|
||||
nvgLineTo(mContext->vg, rec.p3().x, rec.p3().y);
|
||||
|
||||
nvgMoveTo(mContext->vg, rec.p2().x, rec.p2().y);
|
||||
nvgLineTo(mContext->vg, rec.p4().x, rec.p4().y);
|
||||
|
||||
nvgStrokeWidth(mContext->vg, 2);
|
||||
nvgStrokeColor(mContext->vg, { col.r, col.g, col.b, col.a });
|
||||
nvgStroke(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::frame(RectF rec, const RGBA& col, halnf round) {
|
||||
nvgBeginPath(mContext->vg);
|
||||
|
||||
nvgMoveTo(mContext->vg, rec.p1().x, rec.p1().y);
|
||||
nvgLineTo(mContext->vg, rec.p2().x, rec.p2().y);
|
||||
nvgLineTo(mContext->vg, rec.p3().x, rec.p3().y);
|
||||
nvgLineTo(mContext->vg, rec.p4().x, rec.p4().y);
|
||||
nvgLineTo(mContext->vg, rec.p1().x, rec.p1().y);
|
||||
|
||||
nvgStrokeWidth(mContext->vg, 2);
|
||||
nvgStrokeColor(mContext->vg, { col.r, col.g, col.b, col.a });
|
||||
nvgStroke(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::outline(RectF rec, const RGBA& col, halnf round, halnf width) {
|
||||
rec.pos += mOrigin;
|
||||
|
||||
const halnf inset = width / 2;
|
||||
nvgBeginPath(mContext->vg);
|
||||
nvgRoundedRect(
|
||||
mContext->vg, rec.x + inset, rec.y + inset, rec.z - width, rec.w - width, round > inset ? round - inset : 0
|
||||
);
|
||||
nvgStrokeWidth(mContext->vg, width);
|
||||
nvgStrokeColor(mContext->vg, { col.r, col.g, col.b, col.a });
|
||||
nvgStroke(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::circleOutline(Vec2F pos, halnf size, const RGBA& col, halnf width) {
|
||||
pos += mOrigin;
|
||||
|
||||
nvgBeginPath(mContext->vg);
|
||||
nvgCircle(mContext->vg, pos.x, pos.y, size - width / 2);
|
||||
nvgStrokeWidth(mContext->vg, width);
|
||||
nvgStrokeColor(mContext->vg, { col.r, col.g, col.b, col.a });
|
||||
nvgStroke(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::polyline(const Vec2F* points, int count, const RGBA& col, 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);
|
||||
nvgStrokeWidth(mContext->vg, width);
|
||||
nvgStrokeColor(mContext->vg, { col.r, col.g, col.b, col.a });
|
||||
nvgStroke(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::polygon(const Vec2F* points, int count, const RGBA& col) {
|
||||
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);
|
||||
nvgFillColor(mContext->vg, { col.r, col.g, col.b, col.a });
|
||||
nvgFill(mContext->vg);
|
||||
}
|
||||
|
||||
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,
|
||||
{ innerColor.r, innerColor.g, innerColor.b, innerColor.a },
|
||||
{ outerColor.r, outerColor.g, outerColor.b, outerColor.a }
|
||||
);
|
||||
|
||||
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& col, halnf thickness) {
|
||||
start += mOrigin;
|
||||
end += mOrigin;
|
||||
|
||||
nvgBeginPath(mContext->vg);
|
||||
nvgFillColor(mContext->vg, { col.r, col.g, col.b, col.a });
|
||||
nvgMoveTo(mContext->vg, start.x, start.y);
|
||||
nvgLineTo(mContext->vg, end.x, end.y);
|
||||
nvgStrokeWidth(mContext->vg, thickness);
|
||||
nvgStrokeColor(mContext->vg, { col.r, col.g, col.b, col.a });
|
||||
nvgStroke(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::circle(Vec2F pos, halnf size, const RGBA& col) {
|
||||
pos += mOrigin;
|
||||
|
||||
nvgBeginPath(mContext->vg);
|
||||
nvgCircle(mContext->vg, pos.x, pos.y, size);
|
||||
|
||||
nvgFillColor(mContext->vg, { col.r, col.g, col.b, col.a });
|
||||
nvgFill(mContext->vg);
|
||||
}
|
||||
|
||||
void Canvas::setOrigin(const Vec2F& origin) { mOrigin = origin; }
|
||||
|
||||
void Canvas::pushClamp(const RectF& rec) {
|
||||
RectF intersection = rec;
|
||||
if (mScissors.size()) {
|
||||
mScissors.last().calcIntersection(rec, intersection);
|
||||
}
|
||||
nvgScissor(mContext->vg, intersection.x, intersection.y, intersection.z, intersection.w);
|
||||
mScissors.append(intersection);
|
||||
}
|
||||
|
||||
void Canvas::popClamp() {
|
||||
DEBUG_ASSERT(mScissors.size());
|
||||
mScissors.pop();
|
||||
if (mScissors.size()) {
|
||||
const auto& rec = mScissors.last();
|
||||
nvgScissor(mContext->vg, rec.x, rec.y, rec.z, rec.w);
|
||||
} else {
|
||||
nvgResetScissor(mContext->vg);
|
||||
}
|
||||
}
|
||||
|
||||
const RectF& Canvas::getClampedArea() const { return mScissors.last(); }
|
||||
|
||||
bool Canvas::isOutsideClamp(const RectF& rec) const {
|
||||
if (!mScissors.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const RectF& clip = mScissors.last();
|
||||
return rec.x >= clip.x + clip.z || clip.x >= rec.x + rec.z || rec.y >= clip.y + clip.w || clip.y >= rec.y + rec.w;
|
||||
}
|
||||
|
||||
void Canvas::text(
|
||||
const char* string, const RectF& aRec, halnf size, Align align, halnf marging, const RGBA& col, FontFace face
|
||||
) {
|
||||
|
||||
RectF rec = { aRec.x + marging, aRec.y + marging, aRec.z - marging * 2, aRec.w - marging * 2 };
|
||||
rec.pos += mOrigin;
|
||||
if (isOutsideClamp(rec)) {
|
||||
return;
|
||||
}
|
||||
|
||||
pushClamp(rec);
|
||||
|
||||
nvgFontSize(mContext->vg, size);
|
||||
nvgFontFaceId(mContext->vg, mContext->fonts.idFor(face));
|
||||
nvgFillColor(mContext->vg, { col.r, col.g, col.b, col.a });
|
||||
|
||||
float centerX = rec.x;
|
||||
float centerY = rec.y;
|
||||
int alignNVG = 0;
|
||||
|
||||
switch (horizontalPartOf(align)) {
|
||||
case HorizontalAlign::Center:
|
||||
alignNVG |= NVG_ALIGN_CENTER;
|
||||
centerX += rec.z * 0.5f;
|
||||
break;
|
||||
case HorizontalAlign::Left: alignNVG |= NVG_ALIGN_LEFT; break;
|
||||
case HorizontalAlign::Right:
|
||||
alignNVG |= NVG_ALIGN_RIGHT;
|
||||
centerX += rec.z;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (verticalPartOf(align)) {
|
||||
case VerticalAlign::Center:
|
||||
alignNVG |= NVG_ALIGN_MIDDLE;
|
||||
centerY += rec.w * 0.5f;
|
||||
break;
|
||||
case VerticalAlign::Top:
|
||||
alignNVG |= NVG_ALIGN_TOP;
|
||||
centerY += rec.w;
|
||||
break;
|
||||
case VerticalAlign::Bottom: alignNVG |= NVG_ALIGN_BOTTOM; break;
|
||||
}
|
||||
|
||||
nvgTextAlign(mContext->vg, alignNVG);
|
||||
|
||||
if (isItalic(face)) {
|
||||
nvgSave(mContext->vg);
|
||||
nvgTranslate(mContext->vg, centerX, centerY);
|
||||
nvgSkewX(mContext->vg, kItalicSkew);
|
||||
nvgText(mContext->vg, 0, 0, string, nullptr);
|
||||
nvgRestore(mContext->vg);
|
||||
} else {
|
||||
nvgText(mContext->vg, centerX, centerY, string, nullptr);
|
||||
}
|
||||
|
||||
popClamp();
|
||||
}
|
||||
|
||||
void Canvas::rotatedText(const char* string, Vec2F center, halnf size, halnf angle, const RGBA& col) {
|
||||
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, { col.r, col.g, col.b, col.a });
|
||||
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 rec, ImageHandle* image, halnf angle, halnf alpha, halnf rounding) {
|
||||
rec.pos += mOrigin;
|
||||
|
||||
auto imgPaint = nvgImagePattern(mContext->vg, rec.x, rec.y, rec.z, rec.w, angle, image->id, alpha);
|
||||
nvgBeginPath(mContext->vg);
|
||||
nvgRoundedRect(mContext->vg, rec.x, rec.y, rec.z, rec.w, rounding);
|
||||
nvgFillPaint(mContext->vg, imgPaint);
|
||||
nvgFill(mContext->vg);
|
||||
}
|
||||
18
GraphicApplication/src/canvas/private/CanvasContext.hpp
Normal file
18
GraphicApplication/src/canvas/private/CanvasContext.hpp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#pragma once
|
||||
|
||||
#include "Canvas.hpp"
|
||||
#include "CanvasFontLoader.hpp"
|
||||
|
||||
#include <nanovg.h>
|
||||
|
||||
namespace tp {
|
||||
|
||||
class Canvas::Context {
|
||||
public:
|
||||
NVGcontext* vg{};
|
||||
Window* window{};
|
||||
CanvasFontIds fonts;
|
||||
};
|
||||
|
||||
inline NVGcolor toNVGColor(const RGBA& color) { return { color.r, color.g, color.b, color.a }; }
|
||||
}
|
||||
90
GraphicApplication/src/canvas/private/CanvasFontLoader.cpp
Normal file
90
GraphicApplication/src/canvas/private/CanvasFontLoader.cpp
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
#include "CanvasFontLoader.hpp"
|
||||
#include "CanvasFonts.hpp"
|
||||
|
||||
#include "Assert.hpp"
|
||||
|
||||
#include <nanovg.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace tp;
|
||||
|
||||
namespace {
|
||||
bool isBold(Canvas::FontFace face) { return face == Canvas::FontFace::Bold || face == Canvas::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(Canvas::FontFace face) const {
|
||||
if (face == Canvas::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) {
|
||||
ASSERT(!"Cant create NVG font")
|
||||
}
|
||||
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(Canvas::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);
|
||||
}
|
||||
76
GraphicApplication/src/canvas/private/CanvasImages.cpp
Normal file
76
GraphicApplication/src/canvas/private/CanvasImages.cpp
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#include "CanvasContext.hpp"
|
||||
|
||||
#include "RenderTargetRegistry.hpp"
|
||||
#include "VulkanContext.hpp"
|
||||
|
||||
#include <nanovg_vk.h>
|
||||
|
||||
using namespace tp;
|
||||
|
||||
Canvas::ImageHandle Canvas::createImageFromTextId(ualni id, Vec2F size) {
|
||||
RenderTargetEntry entry;
|
||||
if (!mContext->window->vulkan()->renderTargets().resolve((uint32_t) id, entry)) {
|
||||
return { 0 };
|
||||
}
|
||||
int image = nvgCreateImageFromHandleVk(
|
||||
mContext->vg,
|
||||
entry.image,
|
||||
entry.view,
|
||||
entry.sampler,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
entry.width,
|
||||
entry.height,
|
||||
0
|
||||
);
|
||||
return { (ualni) image };
|
||||
}
|
||||
|
||||
void Canvas::updateTextureID(ImageHandle handle, ualni id) {
|
||||
RenderTargetEntry entry;
|
||||
if (!mContext->window->vulkan()->renderTargets().resolve((uint32_t) id, entry)) {
|
||||
return;
|
||||
}
|
||||
nvgUpdateImageFromHandleVk(
|
||||
mContext->vg,
|
||||
(int) handle.id,
|
||||
entry.image,
|
||||
entry.view,
|
||||
entry.sampler,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
entry.width,
|
||||
entry.height
|
||||
);
|
||||
}
|
||||
|
||||
Canvas::ImageHandle Canvas::createImageFromFile(const char* path) {
|
||||
int image = nvgCreateImage(mContext->vg, path, 0);
|
||||
return { (ualni) image };
|
||||
}
|
||||
|
||||
Vec2F Canvas::imageSize(ImageHandle image) {
|
||||
if (!image.id) {
|
||||
return { 0, 0 };
|
||||
}
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
nvgImageSize(mContext->vg, (int) image.id, &width, &height);
|
||||
return { (halnf) width, (halnf) height };
|
||||
}
|
||||
|
||||
Canvas::ImageHandle Canvas::createImageFromMemoryRGBA(int width, int height, const unsigned char* rgba) {
|
||||
int image = nvgCreateImageRGBA(mContext->vg, width, height, 0, rgba);
|
||||
return { (ualni) image };
|
||||
}
|
||||
|
||||
void Canvas::updateImageRGBA(ImageHandle image, const unsigned char* rgba) {
|
||||
if (image.id) {
|
||||
nvgUpdateImage(mContext->vg, (int) image.id, rgba);
|
||||
}
|
||||
}
|
||||
|
||||
void Canvas::deleteImageHandle(ImageHandle image) {
|
||||
if (image.id) {
|
||||
nvgDeleteImage(mContext->vg, image.id);
|
||||
}
|
||||
}
|
||||
142
GraphicApplication/src/canvas/public/Canvas.hpp
Normal file
142
GraphicApplication/src/canvas/public/Canvas.hpp
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
#pragma once
|
||||
|
||||
#include "Window.hpp"
|
||||
|
||||
#include "Color.hpp"
|
||||
#include "Rect.hpp"
|
||||
|
||||
#include "Buffer.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace tp {
|
||||
|
||||
class Canvas {
|
||||
class Context;
|
||||
Context* mContext;
|
||||
|
||||
public:
|
||||
Canvas(Window* window, const std::filesystem::path& resourceDirectory);
|
||||
~Canvas();
|
||||
|
||||
Canvas(const Canvas&) = delete;
|
||||
Canvas& operator=(const Canvas&) = delete;
|
||||
|
||||
void drawBegin();
|
||||
void drawEnd();
|
||||
|
||||
public:
|
||||
enum Align : int2 {
|
||||
CC = 0x0000,
|
||||
CT = 0x0001,
|
||||
CB = 0x0002,
|
||||
LC = 0x0100,
|
||||
LT = 0x0101,
|
||||
LB = 0x0102,
|
||||
RC = 0x0200,
|
||||
RT = 0x0201,
|
||||
RB = 0x0202,
|
||||
};
|
||||
|
||||
enum class FontFace { Regular, Bold, Italic, BoldItalic, Monospace };
|
||||
|
||||
enum class CompositeOperation { SourceOver, DestinationOut, DestinationOver };
|
||||
|
||||
enum class PathWinding { Solid, Hole };
|
||||
|
||||
struct ImageHandle {
|
||||
ualni id = 0;
|
||||
};
|
||||
|
||||
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, clamping 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;
|
||||
|
||||
private:
|
||||
Canvas& mCanvas;
|
||||
};
|
||||
|
||||
void save();
|
||||
void restore();
|
||||
|
||||
void setOrigin(const Vec2F& origin);
|
||||
void setCompositeOperation(CompositeOperation operation);
|
||||
|
||||
void pushClamp(const RectF& rec);
|
||||
void popClamp();
|
||||
[[nodiscard]] const RectF& getClampedArea() const;
|
||||
[[nodiscard]] bool isOutsideClamp(const RectF& rec) const;
|
||||
|
||||
void beginPath();
|
||||
void addPathRect(RectF rec, halnf round = 0);
|
||||
void setPathWinding(PathWinding winding);
|
||||
void fillPath(const RGBA& col);
|
||||
void fillPath(const BoxGradient& gradient);
|
||||
|
||||
void debugCross(RectF rec, const RGBA& col);
|
||||
void rect(RectF rec, const RGBA& col, halnf round = 0);
|
||||
void gradientRect(RectF rec, const RGBA& from, const RGBA& to, bool horizontal, halnf round = 0);
|
||||
void frame(RectF rec, const RGBA& col, halnf round = 0);
|
||||
void outline(RectF rec, const RGBA& col, halnf round, halnf width);
|
||||
void circle(Vec2F pos, halnf size, const RGBA& col);
|
||||
void circleOutline(Vec2F pos, halnf size, const RGBA& col, halnf width);
|
||||
void polyline(const Vec2F* points, int count, const RGBA& col, halnf width);
|
||||
void polygon(const Vec2F* points, int count, const RGBA& col);
|
||||
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*, const RectF&, halnf size, Align, halnf padding, const RGBA&, FontFace face = FontFace::Regular);
|
||||
void rotatedText(const char* text, Vec2F center, halnf size, halnf angle, const RGBA& col);
|
||||
[[nodiscard]] halnf textWidth(const char*, halnf size, FontFace face = FontFace::Regular);
|
||||
void prefixWidths(const char* text, size_t length, halnf size, FontFace face, halnf* widths);
|
||||
void line(Vec2F start, Vec2F end, const RGBA& col, halnf thickness);
|
||||
|
||||
ImageHandle createImageFromTextId(ualni id, Vec2F size);
|
||||
void updateTextureID(ImageHandle handle, ualni id);
|
||||
|
||||
ImageHandle createImageFromFile(const char* path);
|
||||
[[nodiscard]] Vec2F imageSize(ImageHandle image);
|
||||
ImageHandle createImageFromMemoryRGBA(int width, int height, const unsigned char* rgba);
|
||||
void updateImageRGBA(ImageHandle image, const unsigned char* rgba);
|
||||
|
||||
void deleteImageHandle(ImageHandle image);
|
||||
void drawImage(RectF rec, ImageHandle* image, halnf angle = 0, halnf alpha = 1.f, halnf rounding = 0.f);
|
||||
|
||||
private:
|
||||
struct SavedFrame {
|
||||
Vec2F origin;
|
||||
size_t clampDepth = 0;
|
||||
};
|
||||
|
||||
Buffer<RectF> mScissors;
|
||||
Buffer<SavedFrame> mSavedFrames;
|
||||
Vec2F mOrigin = { 0, 0 };
|
||||
};
|
||||
}
|
||||
110
GraphicApplication/src/debug_ui/private/DebugGUI.cpp
Normal file
110
GraphicApplication/src/debug_ui/private/DebugGUI.cpp
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
#include "DebugGUI.hpp"
|
||||
#include "ImGuiPlatform.hpp"
|
||||
#include "VulkanContext.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_impl_vulkan.h>
|
||||
#include <imgui_internal.h>
|
||||
|
||||
#include "implot.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace tp {
|
||||
class DebugGUI::Context {
|
||||
public:
|
||||
ImGuiIO* io{};
|
||||
ImGuiContext* ctx{};
|
||||
VulkanContext* vulkan{};
|
||||
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
|
||||
std::unique_ptr<ImGuiPlatform> platform;
|
||||
std::filesystem::path resourceDirectory;
|
||||
};
|
||||
}
|
||||
|
||||
using namespace tp;
|
||||
|
||||
static VkDescriptorPool createImguiDescriptorPool(VkDevice device) {
|
||||
const std::array<VkDescriptorPoolSize, 1> poolSizes = {
|
||||
VkDescriptorPoolSize{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 64 },
|
||||
};
|
||||
|
||||
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;
|
||||
if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &pool) != VK_SUCCESS) {
|
||||
throw std::runtime_error("failed to create imgui descriptor pool");
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
DebugGUI::DebugGUI(Window* window, const std::filesystem::path& resourceDirectory) {
|
||||
mContext = std::make_unique<Context>();
|
||||
mContext->vulkan = window->vulkan();
|
||||
mContext->resourceDirectory = resourceDirectory;
|
||||
|
||||
IMGUI_CHECKVERSION();
|
||||
mContext->ctx = ImGui::CreateContext();
|
||||
mContext->io = &ImGui::GetIO();
|
||||
|
||||
mContext->platform = std::make_unique<ImGuiPlatform>(*window);
|
||||
|
||||
mContext->descriptorPool = createImguiDescriptorPool(mContext->vulkan->device());
|
||||
|
||||
ImGui_ImplVulkan_InitInfo initInfo{};
|
||||
initInfo.Instance = mContext->vulkan->instance();
|
||||
initInfo.PhysicalDevice = mContext->vulkan->physicalDevice();
|
||||
initInfo.Device = mContext->vulkan->device();
|
||||
initInfo.Queue = mContext->vulkan->graphicsQueue();
|
||||
initInfo.DescriptorPool = mContext->descriptorPool;
|
||||
initInfo.RenderPass = mContext->vulkan->renderPass();
|
||||
initInfo.MinImageCount = mContext->vulkan->framesInFlight();
|
||||
initInfo.ImageCount = mContext->vulkan->swapchainImageCount();
|
||||
initInfo.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
|
||||
ImGui_ImplVulkan_Init(&initInfo);
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
const std::string fontPath = (mContext->resourceDirectory / "Font.ttf").string();
|
||||
io.Fonts->AddFontFromFileTTF(fontPath.c_str(), 18.f);
|
||||
|
||||
io.ConfigInputTrickleEventQueue = false;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
|
||||
ImPlot::CreateContext();
|
||||
}
|
||||
|
||||
DebugGUI::~DebugGUI() {
|
||||
mContext->vulkan->waitIdle();
|
||||
|
||||
ImPlot::DestroyContext();
|
||||
|
||||
ImGui_ImplVulkan_Shutdown();
|
||||
mContext->platform.reset();
|
||||
ImGui::DestroyContext();
|
||||
|
||||
vkDestroyDescriptorPool(mContext->vulkan->device(), mContext->descriptorPool, nullptr);
|
||||
}
|
||||
|
||||
void DebugGUI::drawBegin() {
|
||||
ImGui_ImplVulkan_NewFrame();
|
||||
mContext->platform->newFrame();
|
||||
ImGui::NewFrame();
|
||||
}
|
||||
|
||||
void DebugGUI::drawEnd() {
|
||||
ImGui::Render();
|
||||
mContext->platform->endFrame();
|
||||
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), mContext->vulkan->activeCommandBuffer());
|
||||
}
|
||||
|
||||
void DebugGUI::showValue(const char* label, halnf value) { ImGui::Text("%s: %f", label, value); }
|
||||
319
GraphicApplication/src/debug_ui/private/ImGuiPlatform.cpp
Normal file
319
GraphicApplication/src/debug_ui/private/ImGuiPlatform.cpp
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
#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(InputID key) {
|
||||
static const std::unordered_map<InputID, ImGuiKey> keys = {
|
||||
{ InputID::TAB, ImGuiKey_Tab },
|
||||
{ InputID::LEFT, ImGuiKey_LeftArrow },
|
||||
{ InputID::RIGHT, ImGuiKey_RightArrow },
|
||||
{ InputID::UP, ImGuiKey_UpArrow },
|
||||
{ InputID::DOWN, ImGuiKey_DownArrow },
|
||||
{ InputID::PAGE_UP, ImGuiKey_PageUp },
|
||||
{ InputID::PAGE_DOWN, ImGuiKey_PageDown },
|
||||
{ InputID::HOME, ImGuiKey_Home },
|
||||
{ InputID::END, ImGuiKey_End },
|
||||
{ InputID::INSERT, ImGuiKey_Insert },
|
||||
{ InputID::DELETE_KEY, ImGuiKey_Delete },
|
||||
{ InputID::BACKSPACE, ImGuiKey_Backspace },
|
||||
{ InputID::SPACE, ImGuiKey_Space },
|
||||
{ InputID::ENTER, ImGuiKey_Enter },
|
||||
{ InputID::ESCAPE, ImGuiKey_Escape },
|
||||
{ InputID::LEFT_CONTROL, ImGuiKey_LeftCtrl },
|
||||
{ InputID::LEFT_SHIFT, ImGuiKey_LeftShift },
|
||||
{ InputID::LEFT_ALT, ImGuiKey_LeftAlt },
|
||||
{ InputID::LEFT_SUPER, ImGuiKey_LeftSuper },
|
||||
{ InputID::RIGHT_CONTROL, ImGuiKey_RightCtrl },
|
||||
{ InputID::RIGHT_SHIFT, ImGuiKey_RightShift },
|
||||
{ InputID::RIGHT_ALT, ImGuiKey_RightAlt },
|
||||
{ InputID::RIGHT_SUPER, ImGuiKey_RightSuper },
|
||||
{ InputID::MENU, ImGuiKey_Menu },
|
||||
{ InputID::APOSTROPHE, ImGuiKey_Apostrophe },
|
||||
{ InputID::COMMA, ImGuiKey_Comma },
|
||||
{ InputID::MINUS, ImGuiKey_Minus },
|
||||
{ InputID::PERIOD, ImGuiKey_Period },
|
||||
{ InputID::SLASH, ImGuiKey_Slash },
|
||||
{ InputID::SEMICOLON, ImGuiKey_Semicolon },
|
||||
{ InputID::EQUAL, ImGuiKey_Equal },
|
||||
{ InputID::LEFT_BRACKET, ImGuiKey_LeftBracket },
|
||||
{ InputID::BACKSLASH, ImGuiKey_Backslash },
|
||||
{ InputID::RIGHT_BRACKET, ImGuiKey_RightBracket },
|
||||
{ InputID::GRAVE_ACCENT, ImGuiKey_GraveAccent },
|
||||
{ InputID::CAPS_LOCK, ImGuiKey_CapsLock },
|
||||
{ InputID::SCROLL_LOCK, ImGuiKey_ScrollLock },
|
||||
{ InputID::NUM_LOCK, ImGuiKey_NumLock },
|
||||
{ InputID::PRINT_SCREEN, ImGuiKey_PrintScreen },
|
||||
{ InputID::PAUSE, ImGuiKey_Pause },
|
||||
{ InputID::KP_DECIMAL, ImGuiKey_KeypadDecimal },
|
||||
{ InputID::KP_DIVIDE, ImGuiKey_KeypadDivide },
|
||||
{ InputID::KP_MULTIPLY, ImGuiKey_KeypadMultiply },
|
||||
{ InputID::KP_SUBTRACT, ImGuiKey_KeypadSubtract },
|
||||
{ InputID::KP_ADD, ImGuiKey_KeypadAdd },
|
||||
{ InputID::KP_ENTER, ImGuiKey_KeypadEnter },
|
||||
{ InputID::KP_EQUAL, ImGuiKey_KeypadEqual },
|
||||
};
|
||||
|
||||
const auto found = keys.find(key);
|
||||
if (found != keys.end()) {
|
||||
return found->second;
|
||||
}
|
||||
|
||||
const auto code = (int) key;
|
||||
if (code >= (int) InputID::A && code <= (int) InputID::Z) {
|
||||
return (ImGuiKey) (ImGuiKey_A + code - (int) InputID::A);
|
||||
}
|
||||
if (code >= (int) InputID::N0 && code <= (int) InputID::N9) {
|
||||
return (ImGuiKey) (ImGuiKey_0 + code - (int) InputID::N0);
|
||||
}
|
||||
if (code >= (int) InputID::F1 && code <= (int) InputID::F24) {
|
||||
return (ImGuiKey) (ImGuiKey_F1 + code - (int) InputID::F1);
|
||||
}
|
||||
if (code >= (int) InputID::KP_0 && code <= (int) InputID::KP_9) {
|
||||
return (ImGuiKey) (ImGuiKey_Keypad0 + code - (int) InputID::KP_0);
|
||||
}
|
||||
return ImGuiKey_None;
|
||||
}
|
||||
|
||||
ImGuiKey imguiModifier(InputID key) {
|
||||
switch (key) {
|
||||
case InputID::LEFT_CONTROL:
|
||||
case InputID::RIGHT_CONTROL: return ImGuiMod_Ctrl;
|
||||
case InputID::LEFT_SHIFT:
|
||||
case InputID::RIGHT_SHIFT: return ImGuiMod_Shift;
|
||||
case InputID::LEFT_ALT:
|
||||
case InputID::RIGHT_ALT: return ImGuiMod_Alt;
|
||||
case InputID::LEFT_SUPER:
|
||||
case InputID::RIGHT_SUPER: return ImGuiMod_Super;
|
||||
default: return ImGuiKey_None;
|
||||
}
|
||||
}
|
||||
|
||||
bool isRightModifier(InputID key) {
|
||||
return key == InputID::RIGHT_CONTROL || key == InputID::RIGHT_SHIFT || key == InputID::RIGHT_ALT ||
|
||||
key == InputID::RIGHT_SUPER;
|
||||
}
|
||||
|
||||
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(InputID button) {
|
||||
switch (button) {
|
||||
case InputID::MOUSE1: return ImGuiMouseButton_Left;
|
||||
case InputID::MOUSE2: return ImGuiMouseButton_Right;
|
||||
case InputID::MOUSE3: return ImGuiMouseButton_Middle;
|
||||
case InputID::MOUSE4: return 3;
|
||||
case InputID::MOUSE5: return 4;
|
||||
default: 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 = "modules_wayland";
|
||||
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.getSize();
|
||||
const Vec2F framebuffer = mWindow.getFramebufferSize();
|
||||
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) {
|
||||
for (const InputEvent& event : input.events()) {
|
||||
switch (event.type) {
|
||||
case InputEvent::Type::MOUSE_POS:
|
||||
if (event.mouseEvent.x == kPointerOutsideCoordinate && event.mouseEvent.y == kPointerOutsideCoordinate) {
|
||||
onPointerLeft();
|
||||
} else {
|
||||
onPointerMoved(event.mouseEvent);
|
||||
}
|
||||
break;
|
||||
case InputEvent::Type::BUTTON_ACTION:
|
||||
switch (event.buttonAction) {
|
||||
case InputEvent::ButtonAction::PRESS:
|
||||
onPointerButton(event.id, true);
|
||||
onKey(event.id, true);
|
||||
break;
|
||||
case InputEvent::ButtonAction::RELEASE:
|
||||
onPointerButton(event.id, false);
|
||||
onKey(event.id, false);
|
||||
break;
|
||||
case InputEvent::ButtonAction::REPEAT: onKeyRepeat(event.id); break;
|
||||
case InputEvent::ButtonAction::NONE:
|
||||
case InputEvent::ButtonAction::HOLD: break;
|
||||
}
|
||||
break;
|
||||
case InputEvent::Type::SCROLL:
|
||||
onScrolled(event.scrollDelta, event.continuousScroll ? ScrollSource::Touchpad : ScrollSource::Wheel);
|
||||
break;
|
||||
case InputEvent::Type::TEXT: onText(event.text); break;
|
||||
case InputEvent::Type::FOCUS: onKeyboardFocus(event.focused); break;
|
||||
case InputEvent::Type::NONE:
|
||||
case InputEvent::Type::MOUSE_DELTA:
|
||||
case InputEvent::Type::SCROLL_END:
|
||||
case InputEvent::Type::POINTER_SAMPLE:
|
||||
case InputEvent::Type::GESTURE:
|
||||
case InputEvent::Type::RESIZED: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiPlatform::onPointerMoved(Vec2F contentPosition) {
|
||||
ImGui::GetIO().AddMousePosEvent(contentPosition.x, contentPosition.y);
|
||||
}
|
||||
|
||||
void ImGuiPlatform::onPointerLeft() { ImGui::GetIO().AddMousePosEvent(-FLT_MAX, -FLT_MAX); }
|
||||
|
||||
void ImGuiPlatform::onPointerButton(InputID button, bool pressed) {
|
||||
const int index = imguiMouseButton(button);
|
||||
if (index >= 0) {
|
||||
ImGui::GetIO().AddMouseButtonEvent(index, pressed);
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiPlatform::onScrolled(Vec2F delta, ScrollSource) { ImGui::GetIO().AddMouseWheelEvent(delta.x, delta.y); }
|
||||
|
||||
void ImGuiPlatform::onKey(InputID key, bool pressed) {
|
||||
updateModifier(key, pressed);
|
||||
|
||||
const ImGuiKey imgui = imguiKey(key);
|
||||
if (imgui != ImGuiKey_None) {
|
||||
ImGui::GetIO().AddKeyEvent(imgui, pressed);
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiPlatform::onKeyRepeat(InputID key) { onKey(key, true); }
|
||||
|
||||
void ImGuiPlatform::onText(const std::string& utf8) { ImGui::GetIO().AddInputCharactersUTF8(utf8.c_str()); }
|
||||
|
||||
void ImGuiPlatform::onKeyboardFocus(bool focused) { ImGui::GetIO().AddFocusEvent(focused); }
|
||||
|
||||
void ImGuiPlatform::updateModifier(InputID 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 });
|
||||
}
|
||||
50
GraphicApplication/src/debug_ui/private/ImGuiPlatform.hpp
Normal file
50
GraphicApplication/src/debug_ui/private/ImGuiPlatform.hpp
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#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 onPointerMoved(Vec2F contentPosition);
|
||||
void onPointerLeft();
|
||||
void onPointerButton(InputID button, bool pressed);
|
||||
void onScrolled(Vec2F delta, ScrollSource source);
|
||||
void onKey(InputID key, bool pressed);
|
||||
void onKeyRepeat(InputID key);
|
||||
void onText(const std::string& utf8);
|
||||
void onKeyboardFocus(bool focused);
|
||||
|
||||
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(InputID key, bool pressed);
|
||||
|
||||
Window& mWindow;
|
||||
std::string mClipboard;
|
||||
double mPreviousTime = 0;
|
||||
int mCursor = -2;
|
||||
bool mModifierSides[4][2] = {};
|
||||
bool mTextInputWanted = false;
|
||||
};
|
||||
}
|
||||
28
GraphicApplication/src/debug_ui/public/DebugGUI.hpp
Normal file
28
GraphicApplication/src/debug_ui/public/DebugGUI.hpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include "Window.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
namespace tp {
|
||||
|
||||
class DebugGUI {
|
||||
class Context;
|
||||
|
||||
public:
|
||||
DebugGUI(Window* window, const std::filesystem::path& resourceDirectory);
|
||||
~DebugGUI();
|
||||
|
||||
DebugGUI(const DebugGUI&) = delete;
|
||||
DebugGUI& operator=(const DebugGUI&) = delete;
|
||||
|
||||
void drawBegin();
|
||||
void drawEnd();
|
||||
|
||||
void showValue(const char* label, halnf value);
|
||||
|
||||
private:
|
||||
std::unique_ptr<Context> mContext;
|
||||
};
|
||||
}
|
||||
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 "UiTrail.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) :
|
||||
mSettings(std::move(settings)) {
|
||||
mSettings.directory = directoryFor(mSettings.directory.empty() ? defaultHangsDirectory() : mSettings.directory);
|
||||
if (!mSettings.clockNs) {
|
||||
mSettings.clockNs = nowNs;
|
||||
}
|
||||
mPhaseStartNs.store(now());
|
||||
allowDebuggerAttach();
|
||||
StackTrace::prepareThreadSampling();
|
||||
mUiTid = StackTrace::callingThreadId();
|
||||
UiTrail::trackCallingThread();
|
||||
pruneOldReports();
|
||||
mThread = std::thread([this]() { watch(); });
|
||||
}
|
||||
|
||||
HangWatchdog::~HangWatchdog() {
|
||||
{
|
||||
std::lock_guard lock(mStopMutex);
|
||||
mStopping = true;
|
||||
}
|
||||
mStopped.notify_all();
|
||||
mThread.join();
|
||||
UiTrail::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::noteInputEvent() { mInputEvents.fetch_add(1, 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 = UiTrail::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
214
GraphicApplication/src/diagnostics/private/Profiler.cpp
Normal file
214
GraphicApplication/src/diagnostics/private/Profiler.cpp
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
#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;
|
||||
}
|
||||
|
||||
Profiler& Profiler::global() {
|
||||
static Profiler profiler;
|
||||
return profiler;
|
||||
}
|
||||
|
||||
void Profiler::setEnabled(bool enabled) {
|
||||
mEnabled = enabled;
|
||||
if (!enabled) {
|
||||
mInFrame = false;
|
||||
}
|
||||
}
|
||||
|
||||
ProfileId Profiler::intern(std::vector<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::vector<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); }
|
||||
|
||||
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(ProfileId section) :
|
||||
mSection(section) {
|
||||
if (Profiler::global().isInFrame()) {
|
||||
mStartNs = monotonicNs();
|
||||
}
|
||||
}
|
||||
|
||||
ProfileScope::~ProfileScope() {
|
||||
if (mStartNs < 0) {
|
||||
return;
|
||||
}
|
||||
Profiler::global().addSectionTime(mSection, (double) (monotonicNs() - mStartNs) / 1.0e6);
|
||||
}
|
||||
}
|
||||
80
GraphicApplication/src/diagnostics/private/UiTrail.cpp
Normal file
80
GraphicApplication/src/diagnostics/private/UiTrail.cpp
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
#include "UiTrail.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
struct Trail {
|
||||
std::atomic<const char*> names[UiTrail::kDepth] = {};
|
||||
std::atomic<int> depth{ 0 };
|
||||
std::atomic<int> overflow{ 0 };
|
||||
};
|
||||
|
||||
Trail& trail() {
|
||||
static Trail shared;
|
||||
return shared;
|
||||
}
|
||||
|
||||
std::atomic<std::thread::id>& trackedThread() {
|
||||
static std::atomic<std::thread::id> tracked{};
|
||||
return tracked;
|
||||
}
|
||||
}
|
||||
|
||||
void UiTrail::push(const char* name) {
|
||||
if (!isTracked()) {
|
||||
return;
|
||||
}
|
||||
Trail& shared = trail();
|
||||
const int depth = shared.depth.load(std::memory_order_relaxed);
|
||||
if (depth < kDepth) {
|
||||
shared.names[depth].store(name, std::memory_order_relaxed);
|
||||
} else {
|
||||
shared.overflow.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
shared.depth.store(depth + 1, std::memory_order_release);
|
||||
}
|
||||
|
||||
void UiTrail::pop() {
|
||||
if (!isTracked()) {
|
||||
return;
|
||||
}
|
||||
Trail& shared = trail();
|
||||
const int depth = shared.depth.load(std::memory_order_relaxed);
|
||||
if (depth <= 0) {
|
||||
return;
|
||||
}
|
||||
if (depth > kDepth) {
|
||||
shared.overflow.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
shared.depth.store(depth - 1, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool UiTrail::isTracked() { return trackedThread().load(std::memory_order_relaxed) == std::this_thread::get_id(); }
|
||||
|
||||
void UiTrail::trackCallingThread() {
|
||||
trail().depth.store(0);
|
||||
trail().overflow.store(0);
|
||||
trackedThread().store(std::this_thread::get_id());
|
||||
}
|
||||
|
||||
void UiTrail::stopTracking() { trackedThread().store(std::thread::id{}); }
|
||||
|
||||
std::vector<std::string> UiTrail::snapshot() {
|
||||
Trail& shared = trail();
|
||||
const int depth = std::min(shared.depth.load(std::memory_order_acquire), kDepth);
|
||||
std::vector<std::string> names;
|
||||
for (int index = 0; index < depth; index++) {
|
||||
const char* name = shared.names[index].load(std::memory_order_relaxed);
|
||||
names.emplace_back(name ? name : "?");
|
||||
}
|
||||
const int overflow = shared.overflow.load(std::memory_order_relaxed);
|
||||
if (overflow > 0) {
|
||||
names.push_back("... " + std::to_string(overflow) + " deeper");
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
||||
19
GraphicApplication/src/diagnostics/private/UiTrail.hpp
Normal file
19
GraphicApplication/src/diagnostics/private/UiTrail.hpp
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
class UiTrail {
|
||||
public:
|
||||
static constexpr int kDepth = 32;
|
||||
|
||||
static void push(const char* name);
|
||||
static void pop();
|
||||
[[nodiscard]] static bool isTracked();
|
||||
static void trackCallingThread();
|
||||
static void stopTracking();
|
||||
[[nodiscard]] static std::vector<std::string> snapshot();
|
||||
};
|
||||
}
|
||||
120
GraphicApplication/src/diagnostics/public/HangWatchdog.hpp
Normal file
120
GraphicApplication/src/diagnostics/public/HangWatchdog.hpp
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#pragma once
|
||||
|
||||
#include "StackTrace.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>()>;
|
||||
|
||||
explicit HangWatchdog(HangSettings settings);
|
||||
~HangWatchdog();
|
||||
|
||||
HangWatchdog(const HangWatchdog&) = delete;
|
||||
HangWatchdog& operator=(const HangWatchdog&) = delete;
|
||||
|
||||
void enterPhase(UiPhase phase);
|
||||
void noteInputEvent();
|
||||
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;
|
||||
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;
|
||||
};
|
||||
}
|
||||
126
GraphicApplication/src/diagnostics/public/Profiler.hpp
Normal file
126
GraphicApplication/src/diagnostics/public/Profiler.hpp
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#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;
|
||||
|
||||
[[nodiscard]] static Profiler& global();
|
||||
|
||||
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::vector<std::string>& sectionNames() const { return mSectionNames; }
|
||||
[[nodiscard]] const std::vector<std::string>& counterNames() const { return mCounterNames; }
|
||||
|
||||
[[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;
|
||||
|
||||
[[nodiscard]] static double threadCpuMs();
|
||||
[[nodiscard]] static double processCpuMs();
|
||||
[[nodiscard]] static double peakResidentMegabytes();
|
||||
|
||||
private:
|
||||
static ProfileId intern(std::vector<std::string>& names, const char* name);
|
||||
[[nodiscard]] static ptrdiff_t indexOf(const std::vector<std::string>& names, const std::string& name);
|
||||
|
||||
bool mEnabled = false;
|
||||
bool mInFrame = false;
|
||||
uint64_t mNextFrame = 0;
|
||||
double mFrameWallStart = 0;
|
||||
double mFrameCpuStart = 0;
|
||||
|
||||
std::vector<std::string> mSectionNames;
|
||||
std::vector<std::string> mCounterNames;
|
||||
|
||||
std::array<double, kMaxNames> mSectionMs{};
|
||||
std::array<uint32_t, kMaxNames> mSectionCalls{};
|
||||
std::array<double, kMaxNames> mCounters{};
|
||||
|
||||
std::vector<FrameProfile> mFrames;
|
||||
};
|
||||
|
||||
class ProfileScope {
|
||||
public:
|
||||
ProfileScope(ProfileId section);
|
||||
~ProfileScope();
|
||||
|
||||
ProfileScope(const ProfileScope&) = delete;
|
||||
ProfileScope& operator=(const ProfileScope&) = delete;
|
||||
|
||||
private:
|
||||
ProfileId mSection;
|
||||
int64_t mStartNs = -1;
|
||||
};
|
||||
|
||||
inline void profileCount(ProfileId counter, double amount = 1) {
|
||||
Profiler& profiler = Profiler::global();
|
||||
if (profiler.isEnabled()) {
|
||||
profiler.addCounter(counter, amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#define TP_PROFILE_CONCAT_INNER(a, b) a##b
|
||||
#define TP_PROFILE_CONCAT(a, b) TP_PROFILE_CONCAT_INNER(a, b)
|
||||
|
||||
#define TP_PROFILE_SCOPE(name) \
|
||||
static const ::tp::ProfileId TP_PROFILE_CONCAT(tpProfileId, __LINE__) = ::tp::Profiler::global().section(name); \
|
||||
const ::tp::ProfileScope TP_PROFILE_CONCAT(tpProfileScope, __LINE__)(TP_PROFILE_CONCAT(tpProfileId, __LINE__))
|
||||
|
||||
#define TP_PROFILE_COUNT(name, amount) \
|
||||
do { \
|
||||
static const ::tp::ProfileId tpProfileCounterId = ::tp::Profiler::global().counter(name); \
|
||||
::tp::profileCount(tpProfileCounterId, (double) (amount)); \
|
||||
} while (false)
|
||||
198
GraphicApplication/src/input_layer/private/InputCoalescer.cpp
Normal file
198
GraphicApplication/src/input_layer/private/InputCoalescer.cpp
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
#include "InputCoalescer.hpp"
|
||||
|
||||
#include "InputEventSink.hpp"
|
||||
|
||||
using namespace tp;
|
||||
|
||||
class InputCoalescer::Recorder : public InputEventSink {
|
||||
public:
|
||||
explicit Recorder(InputCoalescer& coalescer) :
|
||||
mCoalescer(coalescer) {}
|
||||
|
||||
void onPointerSample(const PointerSample& sample) override {
|
||||
if (tap()) {
|
||||
tap()->onPointerSample(sample);
|
||||
}
|
||||
mCoalescer.pointerSample(sample);
|
||||
}
|
||||
void onPointerMoved(Vec2F position) override {
|
||||
if (tap()) {
|
||||
tap()->onPointerMoved(position);
|
||||
}
|
||||
mCoalescer.pointerMoved(position);
|
||||
}
|
||||
void onPointerLeft() override {
|
||||
if (tap()) {
|
||||
tap()->onPointerLeft();
|
||||
}
|
||||
mCoalescer.pointerLeft();
|
||||
}
|
||||
void onPointerButton(InputID button, bool pressed) override {
|
||||
if (tap()) {
|
||||
tap()->onPointerButton(button, pressed);
|
||||
}
|
||||
mCoalescer.pointerButton(button, pressed);
|
||||
}
|
||||
void onScrolled(Vec2F delta, ScrollSource source) override {
|
||||
if (tap()) {
|
||||
tap()->onScrolled(delta, source);
|
||||
}
|
||||
mCoalescer.scrolled(delta, source);
|
||||
}
|
||||
void onScrollEnded() override {
|
||||
if (tap()) {
|
||||
tap()->onScrollEnded();
|
||||
}
|
||||
mCoalescer.scrollEnded();
|
||||
}
|
||||
void onKey(InputID key, bool pressed) override {
|
||||
if (tap()) {
|
||||
tap()->onKey(key, pressed);
|
||||
}
|
||||
mCoalescer.key(key, pressed);
|
||||
}
|
||||
void onKeyRepeat(InputID key) override {
|
||||
if (tap()) {
|
||||
tap()->onKeyRepeat(key);
|
||||
}
|
||||
mCoalescer.keyRepeat(key);
|
||||
}
|
||||
void onText(const std::string& utf8) override {
|
||||
if (tap()) {
|
||||
tap()->onText(utf8);
|
||||
}
|
||||
mCoalescer.text(utf8);
|
||||
}
|
||||
void onKeyboardFocus(bool focused) override {
|
||||
if (tap()) {
|
||||
tap()->onKeyboardFocus(focused);
|
||||
}
|
||||
mCoalescer.keyboardFocus(focused);
|
||||
}
|
||||
void onGesture(const TouchGesture& gesture) override {
|
||||
if (tap()) {
|
||||
tap()->onGesture(gesture);
|
||||
}
|
||||
mCoalescer.gesture(gesture);
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] InputEventSink* tap() const { return mCoalescer.tap(); }
|
||||
|
||||
InputCoalescer& mCoalescer;
|
||||
};
|
||||
|
||||
InputCoalescer::InputCoalescer() :
|
||||
mRecorder(std::make_unique<Recorder>(*this)) {}
|
||||
|
||||
InputCoalescer::~InputCoalescer() = default;
|
||||
|
||||
InputEventSink& InputCoalescer::recorder() { return *mRecorder; }
|
||||
|
||||
void InputCoalescer::push(Pending event, DeliveryPolicy policy) {
|
||||
mStatistics.received++;
|
||||
|
||||
if (policy != DeliveryPolicy::Every && !mEvents.empty() && mEvents.back().kind == event.kind) {
|
||||
Pending& previous = mEvents.back();
|
||||
if (policy == DeliveryPolicy::Latest) {
|
||||
previous.vector = event.vector;
|
||||
} else {
|
||||
previous.vector += event.vector;
|
||||
}
|
||||
previous.sample = event.sample;
|
||||
return;
|
||||
}
|
||||
|
||||
mEvents.push_back(std::move(event));
|
||||
}
|
||||
|
||||
void InputCoalescer::pointerMoved(Vec2F position) {
|
||||
push({ Kind::Motion, position, InputID::SPACE, false, {}, mSample }, mPolicies.pointerMotion);
|
||||
}
|
||||
|
||||
void InputCoalescer::pointerLeft() { push({ Kind::Leave }, DeliveryPolicy::Every); }
|
||||
|
||||
void InputCoalescer::pointerButton(InputID button, bool pressed) {
|
||||
push({ Kind::Button, { 0, 0 }, button, pressed, {}, mSample }, DeliveryPolicy::Every);
|
||||
}
|
||||
|
||||
void InputCoalescer::scrolled(Vec2F delta, ScrollSource source) {
|
||||
Pending event{ Kind::Scroll, delta };
|
||||
event.flag = source == ScrollSource::Touchpad;
|
||||
const bool followsOtherSource =
|
||||
!mEvents.empty() && mEvents.back().kind == Kind::Scroll && mEvents.back().flag != event.flag;
|
||||
push(std::move(event), followsOtherSource ? DeliveryPolicy::Every : mPolicies.scroll);
|
||||
}
|
||||
|
||||
void InputCoalescer::scrollEnded() { push({ Kind::ScrollEnd }, DeliveryPolicy::Every); }
|
||||
|
||||
void InputCoalescer::key(InputID key, bool pressed) {
|
||||
push({ Kind::Key, { 0, 0 }, key, pressed }, DeliveryPolicy::Every);
|
||||
}
|
||||
|
||||
void InputCoalescer::keyRepeat(InputID key) { push({ Kind::Repeat, { 0, 0 }, key }, DeliveryPolicy::Every); }
|
||||
|
||||
void InputCoalescer::text(const std::string& utf8) {
|
||||
push({ Kind::Text, { 0, 0 }, InputID::SPACE, false, utf8 }, DeliveryPolicy::Every);
|
||||
}
|
||||
|
||||
void InputCoalescer::keyboardFocus(bool focused) {
|
||||
push({ Kind::Focus, { 0, 0 }, InputID::SPACE, focused }, DeliveryPolicy::Every);
|
||||
}
|
||||
|
||||
void InputCoalescer::gesture(const TouchGesture& gesture) {
|
||||
mStatistics.received++;
|
||||
if (
|
||||
gesture.phase == GesturePhase::Update && !mEvents.empty() && mEvents.back().kind == Kind::Gesture &&
|
||||
mEvents.back().gesture.phase == GesturePhase::Update
|
||||
) {
|
||||
accumulate(mEvents.back().gesture, gesture);
|
||||
return;
|
||||
}
|
||||
Pending event{ Kind::Gesture };
|
||||
event.gesture = gesture;
|
||||
mEvents.push_back(std::move(event));
|
||||
}
|
||||
|
||||
void InputCoalescer::configure(const WindowConfigure& configure) {
|
||||
mStatistics.configuresReceived++;
|
||||
mConfigure = configure;
|
||||
mHasConfigure = true;
|
||||
}
|
||||
|
||||
WindowConfigure InputCoalescer::takeConfigure() {
|
||||
mHasConfigure = false;
|
||||
mStatistics.configuresApplied++;
|
||||
return mConfigure;
|
||||
}
|
||||
|
||||
void InputCoalescer::deliver(InputEventSink& observer) {
|
||||
std::vector<Pending> events;
|
||||
events.swap(mEvents);
|
||||
|
||||
for (const Pending& event : events) {
|
||||
if (event.kind == Kind::Scroll && event.vector == Vec2F{ 0, 0 }) {
|
||||
continue;
|
||||
}
|
||||
mStatistics.delivered++;
|
||||
const bool carriesSample = event.kind == Kind::Motion || event.kind == Kind::Button;
|
||||
if (carriesSample && !(event.sample == mDeliveredSample)) {
|
||||
mDeliveredSample = event.sample;
|
||||
observer.onPointerSample(event.sample);
|
||||
}
|
||||
switch (event.kind) {
|
||||
case Kind::Motion: observer.onPointerMoved(event.vector); break;
|
||||
case Kind::Leave: observer.onPointerLeft(); break;
|
||||
case Kind::Button: observer.onPointerButton(event.id, event.flag); break;
|
||||
case Kind::Scroll:
|
||||
observer.onScrolled(event.vector, event.flag ? ScrollSource::Touchpad : ScrollSource::Wheel);
|
||||
break;
|
||||
case Kind::ScrollEnd: observer.onScrollEnded(); break;
|
||||
case Kind::Key: observer.onKey(event.id, event.flag); break;
|
||||
case Kind::Repeat: observer.onKeyRepeat(event.id); break;
|
||||
case Kind::Text: observer.onText(event.text); break;
|
||||
case Kind::Focus: observer.onKeyboardFocus(event.flag); break;
|
||||
case Kind::Gesture: observer.onGesture(event.gesture); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
331
GraphicApplication/src/input_layer/private/InputRecording.cpp
Normal file
331
GraphicApplication/src/input_layer/private/InputRecording.cpp
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
#include "InputRecording.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
const char* const kKindNames[] = { "sample", "move", "leave", "button", "scroll",
|
||||
"key", "focus", "touchpad-scroll", "scroll-end" };
|
||||
|
||||
const char* const kDeviceNames[] = { "mouse", "finger", "stylus" };
|
||||
const char* const kTipNames[] = { "pen", "eraser", "brush", "pencil", "airbrush" };
|
||||
|
||||
struct LegacyTool {
|
||||
PointerDevice device;
|
||||
StylusTip tip;
|
||||
};
|
||||
|
||||
const LegacyTool kLegacyTools[] = {
|
||||
{ PointerDevice::Mouse, StylusTip::Pen }, { PointerDevice::Stylus, StylusTip::Pen },
|
||||
{ PointerDevice::Stylus, StylusTip::Eraser }, { PointerDevice::Stylus, StylusTip::Brush },
|
||||
{ PointerDevice::Stylus, StylusTip::Pencil }, { PointerDevice::Stylus, StylusTip::Airbrush },
|
||||
{ PointerDevice::Finger, StylusTip::Pen }, { PointerDevice::Mouse, StylusTip::Pen },
|
||||
};
|
||||
|
||||
template <typename Enum, size_t Count>
|
||||
std::optional<Enum> enumNamed(const char* const (&names)[Count], const std::string& name) {
|
||||
for (size_t index = 0; index < Count; index++) {
|
||||
if (name == names[index]) {
|
||||
return (Enum) index;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool readPointerKind(std::istringstream& fields, PointerSample& sample) {
|
||||
std::string device;
|
||||
fields >> device;
|
||||
if (!device.empty() && std::isdigit((unsigned char) device.front())) {
|
||||
const size_t legacy = std::stoul(device);
|
||||
if (legacy >= std::size(kLegacyTools)) {
|
||||
return false;
|
||||
}
|
||||
sample.device = kLegacyTools[legacy].device;
|
||||
sample.tip = kLegacyTools[legacy].tip;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string tip;
|
||||
fields >> tip;
|
||||
const std::optional<PointerDevice> namedDevice = enumNamed<PointerDevice>(kDeviceNames, device);
|
||||
const std::optional<StylusTip> namedTip = enumNamed<StylusTip>(kTipNames, tip);
|
||||
if (!namedDevice || !namedTip) {
|
||||
return false;
|
||||
}
|
||||
sample.device = *namedDevice;
|
||||
sample.tip = *namedTip;
|
||||
return true;
|
||||
}
|
||||
|
||||
double steadyMs() {
|
||||
const auto now = std::chrono::steady_clock::now().time_since_epoch();
|
||||
return std::chrono::duration<double, std::milli>(now).count();
|
||||
}
|
||||
|
||||
bool isPointerKind(RecordedInput::Kind kind) {
|
||||
return kind == RecordedInput::Kind::Sample || kind == RecordedInput::Kind::Move ||
|
||||
kind == RecordedInput::Kind::Button || kind == RecordedInput::Kind::Leave;
|
||||
}
|
||||
}
|
||||
|
||||
void RecordedInput::deliverTo(InputEventSink& observer) const {
|
||||
switch (kind) {
|
||||
case Kind::Sample: observer.onPointerSample(sample); break;
|
||||
case Kind::Move: observer.onPointerMoved(vector); break;
|
||||
case Kind::Leave: observer.onPointerLeft(); break;
|
||||
case Kind::Button: observer.onPointerButton(id, flag); break;
|
||||
case Kind::Scroll: observer.onScrolled(vector, ScrollSource::Wheel); break;
|
||||
case Kind::TouchpadScroll: observer.onScrolled(vector, ScrollSource::Touchpad); break;
|
||||
case Kind::ScrollEnd: observer.onScrollEnded(); break;
|
||||
case Kind::Key: observer.onKey(id, flag); break;
|
||||
case Kind::Focus: observer.onKeyboardFocus(flag); break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string InputRecording::serialize() const {
|
||||
std::string text = std::string(kHeader) + "\n";
|
||||
char line[256];
|
||||
std::snprintf(line, sizeof(line), "window %.1f %.1f\n", windowSize.x, windowSize.y);
|
||||
text += line;
|
||||
|
||||
for (const RecordedInput& event : events) {
|
||||
const char* kind = kKindNames[(size_t) event.kind];
|
||||
switch (event.kind) {
|
||||
case RecordedInput::Kind::Sample:
|
||||
std::snprintf(
|
||||
line,
|
||||
sizeof(line),
|
||||
"%.3f %s %s %s %.5f %.4f %.4f %.4f %.4f %u\n",
|
||||
event.timeMs,
|
||||
kind,
|
||||
kDeviceNames[(size_t) event.sample.device],
|
||||
kTipNames[(size_t) event.sample.tip],
|
||||
event.sample.pressure,
|
||||
event.sample.tilt.x,
|
||||
event.sample.tilt.y,
|
||||
event.sample.rotation,
|
||||
event.sample.distance,
|
||||
event.sample.timeMs
|
||||
);
|
||||
break;
|
||||
case RecordedInput::Kind::Move:
|
||||
case RecordedInput::Kind::Scroll:
|
||||
case RecordedInput::Kind::TouchpadScroll:
|
||||
std::snprintf(line, sizeof(line), "%.3f %s %.3f %.3f\n", event.timeMs, kind, event.vector.x, event.vector.y);
|
||||
break;
|
||||
case RecordedInput::Kind::Button:
|
||||
case RecordedInput::Kind::Key:
|
||||
std::snprintf(
|
||||
line, sizeof(line), "%.3f %s %llu %d\n", event.timeMs, kind, (unsigned long long) event.id, (int) event.flag
|
||||
);
|
||||
break;
|
||||
case RecordedInput::Kind::Focus:
|
||||
std::snprintf(line, sizeof(line), "%.3f %s %d\n", event.timeMs, kind, (int) event.flag);
|
||||
break;
|
||||
case RecordedInput::Kind::Leave:
|
||||
case RecordedInput::Kind::ScrollEnd: std::snprintf(line, sizeof(line), "%.3f %s\n", event.timeMs, kind); break;
|
||||
}
|
||||
text += line;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
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") {
|
||||
fields >> recording.windowSize.x >> recording.windowSize.y;
|
||||
continue;
|
||||
}
|
||||
|
||||
RecordedInput event;
|
||||
std::string kindName;
|
||||
event.timeMs = std::stod(first);
|
||||
fields >> kindName;
|
||||
const std::optional<RecordedInput::Kind> kind = enumNamed<RecordedInput::Kind>(kKindNames, kindName);
|
||||
if (!kind) {
|
||||
return std::nullopt;
|
||||
}
|
||||
event.kind = *kind;
|
||||
|
||||
unsigned long long id = 0;
|
||||
int flag = 0;
|
||||
switch (event.kind) {
|
||||
case RecordedInput::Kind::Sample:
|
||||
if (!readPointerKind(fields, event.sample)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
fields >> event.sample.pressure >> event.sample.tilt.x >> event.sample.tilt.y >> event.sample.rotation >>
|
||||
event.sample.distance >> event.sample.timeMs;
|
||||
break;
|
||||
case RecordedInput::Kind::Move:
|
||||
case RecordedInput::Kind::Scroll:
|
||||
case RecordedInput::Kind::TouchpadScroll: fields >> event.vector.x >> event.vector.y; break;
|
||||
case RecordedInput::Kind::Button:
|
||||
case RecordedInput::Kind::Key:
|
||||
fields >> id >> flag;
|
||||
event.id = (InputID) id;
|
||||
event.flag = flag != 0;
|
||||
break;
|
||||
case RecordedInput::Kind::Focus:
|
||||
fields >> flag;
|
||||
event.flag = flag != 0;
|
||||
break;
|
||||
case RecordedInput::Kind::Leave:
|
||||
case RecordedInput::Kind::ScrollEnd: break;
|
||||
}
|
||||
if (fields.fail()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
recording.events.push_back(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());
|
||||
}
|
||||
|
||||
InputRecording InputRecording::translated(Vec2F offset) const {
|
||||
InputRecording moved = *this;
|
||||
for (RecordedInput& event : moved.events) {
|
||||
if (event.kind == RecordedInput::Kind::Move) {
|
||||
event.vector += 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 (events[index].kind != RecordedInput::Kind::Button) {
|
||||
continue;
|
||||
}
|
||||
first = std::min(first, index);
|
||||
last = index;
|
||||
}
|
||||
if (first >= events.size()) {
|
||||
return sliced;
|
||||
}
|
||||
|
||||
while (first > 0 && isPointerKind(events[first - 1].kind) &&
|
||||
events[first - 1].kind != RecordedInput::Kind::Button) {
|
||||
first--;
|
||||
}
|
||||
const double origin = events[first].timeMs;
|
||||
for (size_t index = first; index <= last; index++) {
|
||||
if (!isPointerKind(events[index].kind)) {
|
||||
continue;
|
||||
}
|
||||
RecordedInput event = events[index];
|
||||
event.timeMs -= origin;
|
||||
sliced.events.push_back(event);
|
||||
}
|
||||
return sliced;
|
||||
}
|
||||
|
||||
InputRecorder::InputRecorder(Clock clock) :
|
||||
mClock(clock ? std::move(clock) : Clock(steadyMs)) {}
|
||||
|
||||
void InputRecorder::record(RecordedInput event) {
|
||||
const double now = mClock();
|
||||
if (mStartMs < 0) {
|
||||
mStartMs = now;
|
||||
}
|
||||
event.timeMs = now - mStartMs;
|
||||
mRecording.events.push_back(event);
|
||||
}
|
||||
|
||||
void InputRecorder::onPointerSample(const PointerSample& sample) {
|
||||
RecordedInput event;
|
||||
event.kind = RecordedInput::Kind::Sample;
|
||||
event.sample = sample;
|
||||
record(event);
|
||||
}
|
||||
|
||||
void InputRecorder::onPointerMoved(Vec2F contentPosition) {
|
||||
RecordedInput event;
|
||||
event.kind = RecordedInput::Kind::Move;
|
||||
event.vector = contentPosition;
|
||||
record(event);
|
||||
}
|
||||
|
||||
void InputRecorder::onPointerLeft() {
|
||||
RecordedInput event;
|
||||
event.kind = RecordedInput::Kind::Leave;
|
||||
record(event);
|
||||
}
|
||||
|
||||
void InputRecorder::onPointerButton(InputID button, bool pressed) {
|
||||
RecordedInput event;
|
||||
event.kind = RecordedInput::Kind::Button;
|
||||
event.id = button;
|
||||
event.flag = pressed;
|
||||
record(event);
|
||||
}
|
||||
|
||||
void InputRecorder::onScrolled(Vec2F delta, ScrollSource source) {
|
||||
RecordedInput event;
|
||||
event.kind = source == ScrollSource::Touchpad ? RecordedInput::Kind::TouchpadScroll : RecordedInput::Kind::Scroll;
|
||||
event.vector = delta;
|
||||
record(event);
|
||||
}
|
||||
|
||||
void InputRecorder::onScrollEnded() {
|
||||
RecordedInput event;
|
||||
event.kind = RecordedInput::Kind::ScrollEnd;
|
||||
record(event);
|
||||
}
|
||||
|
||||
void InputRecorder::onKey(InputID key, bool pressed) {
|
||||
RecordedInput event;
|
||||
event.kind = RecordedInput::Kind::Key;
|
||||
event.id = key;
|
||||
event.flag = pressed;
|
||||
record(event);
|
||||
}
|
||||
|
||||
void InputRecorder::onKeyboardFocus(bool focused) {
|
||||
RecordedInput event;
|
||||
event.kind = RecordedInput::Kind::Focus;
|
||||
event.flag = focused;
|
||||
record(event);
|
||||
}
|
||||
}
|
||||
183
GraphicApplication/src/input_layer/private/InputState.cpp
Normal file
183
GraphicApplication/src/input_layer/private/InputState.cpp
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
#include "InputState.hpp"
|
||||
|
||||
using namespace tp;
|
||||
|
||||
void InputState::onPointerSample(const PointerSample& sample) {
|
||||
InputEvent event;
|
||||
event.type = InputEvent::Type::POINTER_SAMPLE;
|
||||
event.sample = sample;
|
||||
postEvent(InputID::MOUSE1, event);
|
||||
}
|
||||
|
||||
void InputState::onPointerMoved(Vec2F contentPosition) {
|
||||
postEvent(InputID::MOUSE1, { InputEvent::Type::MOUSE_POS, {}, contentPosition });
|
||||
}
|
||||
|
||||
void InputState::onPointerLeft() {
|
||||
releaseHeldButtons();
|
||||
postEvent(
|
||||
InputID::MOUSE1, { InputEvent::Type::MOUSE_POS, {}, { kPointerOutsideCoordinate, kPointerOutsideCoordinate } }
|
||||
);
|
||||
}
|
||||
|
||||
void InputState::onPointerButton(InputID button, bool pressed) {
|
||||
if (pressed) {
|
||||
mHeldPointerButtons.insert(button);
|
||||
} else if (!mHeldPointerButtons.erase(button)) {
|
||||
return;
|
||||
}
|
||||
|
||||
postButton(button, pressed ? InputEvent::ButtonAction::PRESS : InputEvent::ButtonAction::RELEASE);
|
||||
}
|
||||
|
||||
void InputState::onScrolled(Vec2F delta, ScrollSource source) {
|
||||
InputEvent event{ InputEvent::Type::SCROLL, {}, {}, delta };
|
||||
event.continuousScroll = source == ScrollSource::Touchpad;
|
||||
postEvent(InputID::SCROLL, event);
|
||||
}
|
||||
|
||||
void InputState::onScrollEnded() { postEvent(InputID::SCROLL, { InputEvent::Type::SCROLL_END }); }
|
||||
|
||||
void InputState::onKey(InputID key, bool pressed) {
|
||||
if (pressed) {
|
||||
mHeldKeys.insert(key);
|
||||
} else {
|
||||
mHeldKeys.erase(key);
|
||||
}
|
||||
|
||||
postButton(key, pressed ? InputEvent::ButtonAction::PRESS : InputEvent::ButtonAction::RELEASE);
|
||||
}
|
||||
|
||||
void InputState::onKeyRepeat(InputID key) { postButton(key, InputEvent::ButtonAction::REPEAT); }
|
||||
|
||||
void InputState::onText(const std::string& utf8) {
|
||||
InputEvent event;
|
||||
event.type = InputEvent::Type::TEXT;
|
||||
event.text = utf8;
|
||||
postEvent(InputID::NONE, event);
|
||||
}
|
||||
|
||||
void InputState::onKeyboardFocus(bool focused) {
|
||||
if (!focused) {
|
||||
const std::set<InputID> released = mHeldKeys;
|
||||
for (InputID key : released) {
|
||||
onKey(key, false);
|
||||
}
|
||||
}
|
||||
|
||||
InputEvent event;
|
||||
event.type = InputEvent::Type::FOCUS;
|
||||
event.focused = focused;
|
||||
postEvent(InputID::NONE, event);
|
||||
}
|
||||
|
||||
void InputState::onGesture(const TouchGesture& gesture) {
|
||||
InputEvent event;
|
||||
event.type = InputEvent::Type::GESTURE;
|
||||
event.gesture = gesture;
|
||||
postEvent(InputID::NONE, event);
|
||||
}
|
||||
|
||||
void InputState::onResized() { postEvent(InputID::WINDOW_RESIZE, { InputEvent::Type::RESIZED }); }
|
||||
|
||||
void InputState::releaseHeldButtons() {
|
||||
const std::set<InputID> released = mHeldPointerButtons;
|
||||
for (InputID button : released) {
|
||||
onPointerButton(button, false);
|
||||
}
|
||||
}
|
||||
|
||||
void InputState::postButton(InputID id, InputEvent::ButtonAction action) {
|
||||
postEvent(id, { InputEvent::Type::BUTTON_ACTION, action, {} });
|
||||
}
|
||||
|
||||
void InputState::postEvent(InputID inputID, InputEvent inputEvent) {
|
||||
inputEvent.id = inputID;
|
||||
mPendingEvents.push_back(std::move(inputEvent));
|
||||
}
|
||||
|
||||
void InputState::applyPendingEvents() {
|
||||
mPointerPrev = mPointer;
|
||||
mScrollDelta = { 0, 0 };
|
||||
mContinuousScroll = false;
|
||||
mScrollEnded = false;
|
||||
|
||||
for (ButtonState& state : mButtonStates) {
|
||||
state.pressed = false;
|
||||
state.released = false;
|
||||
}
|
||||
|
||||
mFrameEvents.clear();
|
||||
mFrameEvents.swap(mPendingEvents);
|
||||
|
||||
for (const InputEvent& event : mFrameEvents) {
|
||||
applyEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void InputState::applyEvent(const InputEvent& event) {
|
||||
switch (event.type) {
|
||||
case InputEvent::Type::MOUSE_POS: mPointer = event.mouseEvent; break;
|
||||
case InputEvent::Type::MOUSE_DELTA: mPointer += event.mouseEvent; break;
|
||||
case InputEvent::Type::BUTTON_ACTION:
|
||||
{
|
||||
ButtonState& state = mButtonStates[(int) event.id];
|
||||
switch (event.buttonAction) {
|
||||
case InputEvent::ButtonAction::PRESS:
|
||||
if (!state.down) {
|
||||
state.pressed = true;
|
||||
}
|
||||
state.down = true;
|
||||
break;
|
||||
case InputEvent::ButtonAction::HOLD: state.down = true; break;
|
||||
case InputEvent::ButtonAction::RELEASE:
|
||||
if (state.down) {
|
||||
state.released = true;
|
||||
}
|
||||
state.down = false;
|
||||
break;
|
||||
case InputEvent::ButtonAction::NONE:
|
||||
case InputEvent::ButtonAction::REPEAT: break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case InputEvent::Type::SCROLL:
|
||||
mScrollDelta += event.scrollDelta;
|
||||
mContinuousScroll = event.continuousScroll;
|
||||
break;
|
||||
case InputEvent::Type::SCROLL_END: mScrollEnded = true; break;
|
||||
case InputEvent::Type::POINTER_SAMPLE: mPointerSample = event.sample; break;
|
||||
case InputEvent::Type::NONE:
|
||||
case InputEvent::Type::TEXT:
|
||||
case InputEvent::Type::GESTURE:
|
||||
case InputEvent::Type::FOCUS:
|
||||
case InputEvent::Type::RESIZED: break;
|
||||
}
|
||||
}
|
||||
|
||||
void InputState::setCursorOrigin(const Vec2F& origin) { mPointerOrigin = origin; }
|
||||
|
||||
Vec2F InputState::getPointer() const { return mPointer - mPointerOrigin; }
|
||||
|
||||
Vec2F InputState::getPointerPrev() const { return mPointerPrev - mPointerOrigin; }
|
||||
|
||||
Vec2F InputState::getPointerDelta() const { return mPointer - mPointerPrev; }
|
||||
|
||||
bool InputState::isPressed(InputID id) const { return mEnableKeyEvents && mButtonStates[(int) id].pressed; }
|
||||
|
||||
bool InputState::isReleased(InputID id) const { return mEnableKeyEvents && mButtonStates[(int) id].released; }
|
||||
|
||||
bool InputState::isDown(InputID id) const { return mEnableKeyEvents && mButtonStates[(int) id].down; }
|
||||
|
||||
halnf InputState::getPointerPressure() const {
|
||||
if (!mEnableKeyEvents) {
|
||||
return 0;
|
||||
}
|
||||
return mPointerSample.isStylus() ? mPointerSample.pressure : (halnf) isDown(InputID::MOUSE1);
|
||||
}
|
||||
|
||||
halnf InputState::getScrollY() const { return mScrollDelta.y; }
|
||||
|
||||
halnf InputState::getScrollX() const { return mScrollDelta.x; }
|
||||
|
||||
void InputState::setEnableKeyEvents(bool enable) { mEnableKeyEvents = enable; }
|
||||
153
GraphicApplication/src/input_layer/private/KeyMapping.cpp
Normal file
153
GraphicApplication/src/input_layer/private/KeyMapping.cpp
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#include "KeyMapping.hpp"
|
||||
|
||||
#include <linux/input-event-codes.h>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace tp;
|
||||
|
||||
namespace {
|
||||
const std::unordered_map<uint32_t, InputID>& evdevKeys() {
|
||||
static const std::unordered_map<uint32_t, InputID> keys = {
|
||||
{ KEY_SPACE, InputID::SPACE },
|
||||
{ KEY_APOSTROPHE, InputID::APOSTROPHE },
|
||||
{ KEY_COMMA, InputID::COMMA },
|
||||
{ KEY_MINUS, InputID::MINUS },
|
||||
{ KEY_DOT, InputID::PERIOD },
|
||||
{ KEY_SLASH, InputID::SLASH },
|
||||
{ KEY_0, InputID::N0 },
|
||||
{ KEY_1, InputID::N1 },
|
||||
{ KEY_2, InputID::N2 },
|
||||
{ KEY_3, InputID::N3 },
|
||||
{ KEY_4, InputID::N4 },
|
||||
{ KEY_5, InputID::N5 },
|
||||
{ KEY_6, InputID::N6 },
|
||||
{ KEY_7, InputID::N7 },
|
||||
{ KEY_8, InputID::N8 },
|
||||
{ KEY_9, InputID::N9 },
|
||||
{ KEY_SEMICOLON, InputID::SEMICOLON },
|
||||
{ KEY_EQUAL, InputID::EQUAL },
|
||||
{ KEY_A, InputID::A },
|
||||
{ KEY_B, InputID::B },
|
||||
{ KEY_C, InputID::C },
|
||||
{ KEY_D, InputID::D },
|
||||
{ KEY_E, InputID::E },
|
||||
{ KEY_F, InputID::F },
|
||||
{ KEY_G, InputID::G },
|
||||
{ KEY_H, InputID::H },
|
||||
{ KEY_I, InputID::I },
|
||||
{ KEY_J, InputID::J },
|
||||
{ KEY_K, InputID::K },
|
||||
{ KEY_L, InputID::L },
|
||||
{ KEY_M, InputID::M },
|
||||
{ KEY_N, InputID::N },
|
||||
{ KEY_O, InputID::O },
|
||||
{ KEY_P, InputID::P },
|
||||
{ KEY_Q, InputID::Q },
|
||||
{ KEY_R, InputID::R },
|
||||
{ KEY_S, InputID::S },
|
||||
{ KEY_T, InputID::T },
|
||||
{ KEY_U, InputID::U },
|
||||
{ KEY_V, InputID::V },
|
||||
{ KEY_W, InputID::W },
|
||||
{ KEY_X, InputID::X },
|
||||
{ KEY_Y, InputID::Y },
|
||||
{ KEY_Z, InputID::Z },
|
||||
{ KEY_LEFTBRACE, InputID::LEFT_BRACKET },
|
||||
{ KEY_BACKSLASH, InputID::BACKSLASH },
|
||||
{ KEY_RIGHTBRACE, InputID::RIGHT_BRACKET },
|
||||
{ KEY_GRAVE, InputID::GRAVE_ACCENT },
|
||||
{ KEY_102ND, InputID::WORLD_2 },
|
||||
{ KEY_ESC, InputID::ESCAPE },
|
||||
{ KEY_ENTER, InputID::ENTER },
|
||||
{ KEY_TAB, InputID::TAB },
|
||||
{ KEY_BACKSPACE, InputID::BACKSPACE },
|
||||
{ KEY_INSERT, InputID::INSERT },
|
||||
{ KEY_DELETE, InputID::DELETE_KEY },
|
||||
{ KEY_RIGHT, InputID::RIGHT },
|
||||
{ KEY_LEFT, InputID::LEFT },
|
||||
{ KEY_DOWN, InputID::DOWN },
|
||||
{ KEY_UP, InputID::UP },
|
||||
{ KEY_PAGEUP, InputID::PAGE_UP },
|
||||
{ KEY_PAGEDOWN, InputID::PAGE_DOWN },
|
||||
{ KEY_HOME, InputID::HOME },
|
||||
{ KEY_END, InputID::END },
|
||||
{ KEY_CAPSLOCK, InputID::CAPS_LOCK },
|
||||
{ KEY_SCROLLLOCK, InputID::SCROLL_LOCK },
|
||||
{ KEY_NUMLOCK, InputID::NUM_LOCK },
|
||||
{ KEY_SYSRQ, InputID::PRINT_SCREEN },
|
||||
{ KEY_PAUSE, InputID::PAUSE },
|
||||
{ KEY_F1, InputID::F1 },
|
||||
{ KEY_F2, InputID::F2 },
|
||||
{ KEY_F3, InputID::F3 },
|
||||
{ KEY_F4, InputID::F4 },
|
||||
{ KEY_F5, InputID::F5 },
|
||||
{ KEY_F6, InputID::F6 },
|
||||
{ KEY_F7, InputID::F7 },
|
||||
{ KEY_F8, InputID::F8 },
|
||||
{ KEY_F9, InputID::F9 },
|
||||
{ KEY_F10, InputID::F10 },
|
||||
{ KEY_F11, InputID::F11 },
|
||||
{ KEY_F12, InputID::F12 },
|
||||
{ KEY_F13, InputID::F13 },
|
||||
{ KEY_F14, InputID::F14 },
|
||||
{ KEY_F15, InputID::F15 },
|
||||
{ KEY_F16, InputID::F16 },
|
||||
{ KEY_F17, InputID::F17 },
|
||||
{ KEY_F18, InputID::F18 },
|
||||
{ KEY_F19, InputID::F19 },
|
||||
{ KEY_F20, InputID::F20 },
|
||||
{ KEY_F21, InputID::F21 },
|
||||
{ KEY_F22, InputID::F22 },
|
||||
{ KEY_F23, InputID::F23 },
|
||||
{ KEY_F24, InputID::F24 },
|
||||
{ KEY_KP0, InputID::KP_0 },
|
||||
{ KEY_KP1, InputID::KP_1 },
|
||||
{ KEY_KP2, InputID::KP_2 },
|
||||
{ KEY_KP3, InputID::KP_3 },
|
||||
{ KEY_KP4, InputID::KP_4 },
|
||||
{ KEY_KP5, InputID::KP_5 },
|
||||
{ KEY_KP6, InputID::KP_6 },
|
||||
{ KEY_KP7, InputID::KP_7 },
|
||||
{ KEY_KP8, InputID::KP_8 },
|
||||
{ KEY_KP9, InputID::KP_9 },
|
||||
{ KEY_KPDOT, InputID::KP_DECIMAL },
|
||||
{ KEY_KPSLASH, InputID::KP_DIVIDE },
|
||||
{ KEY_KPASTERISK, InputID::KP_MULTIPLY },
|
||||
{ KEY_KPMINUS, InputID::KP_SUBTRACT },
|
||||
{ KEY_KPPLUS, InputID::KP_ADD },
|
||||
{ KEY_KPENTER, InputID::KP_ENTER },
|
||||
{ KEY_KPEQUAL, InputID::KP_EQUAL },
|
||||
{ KEY_LEFTSHIFT, InputID::LEFT_SHIFT },
|
||||
{ KEY_LEFTCTRL, InputID::LEFT_CONTROL },
|
||||
{ KEY_LEFTALT, InputID::LEFT_ALT },
|
||||
{ KEY_LEFTMETA, InputID::LEFT_SUPER },
|
||||
{ KEY_RIGHTSHIFT, InputID::RIGHT_SHIFT },
|
||||
{ KEY_RIGHTCTRL, InputID::RIGHT_CONTROL },
|
||||
{ KEY_RIGHTALT, InputID::RIGHT_ALT },
|
||||
{ KEY_RIGHTMETA, InputID::RIGHT_SUPER },
|
||||
{ KEY_COMPOSE, InputID::MENU },
|
||||
};
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<InputID> tp::inputFromEvdevKey(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<InputID> tp::inputFromPointerButton(uint32_t linuxButton) {
|
||||
switch (linuxButton) {
|
||||
case BTN_LEFT: return InputID::MOUSE1;
|
||||
case BTN_RIGHT: return InputID::MOUSE2;
|
||||
case BTN_MIDDLE: return InputID::MOUSE3;
|
||||
case BTN_SIDE: return InputID::MOUSE4;
|
||||
case BTN_EXTRA: return InputID::MOUSE5;
|
||||
default: return std::nullopt;
|
||||
}
|
||||
}
|
||||
153
GraphicApplication/src/input_layer/private/TouchGesture.cpp
Normal file
153
GraphicApplication/src/input_layer/private/TouchGesture.cpp
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#include "TouchGesture.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
constexpr halnf kMinimumSpan = 1.0f;
|
||||
|
||||
halnf wrapAngle(halnf angle) {
|
||||
constexpr halnf pi = std::numbers::pi_v<halnf>;
|
||||
while (angle > pi) {
|
||||
angle -= 2 * pi;
|
||||
}
|
||||
while (angle < -pi) {
|
||||
angle += 2 * pi;
|
||||
}
|
||||
return angle;
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
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 * halnf(3.14159265358979) / 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 };
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
105
GraphicApplication/src/input_layer/public/InputCoalescer.hpp
Normal file
105
GraphicApplication/src/input_layer/public/InputCoalescer.hpp
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputCodes.hpp"
|
||||
#include "PointerSample.hpp"
|
||||
#include "TouchGesture.hpp"
|
||||
#include "InputEventSink.hpp"
|
||||
#include "Vec.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
enum class DeliveryPolicy { Every, Latest, Accumulate };
|
||||
|
||||
struct InputDeliveryPolicies {
|
||||
DeliveryPolicy pointerMotion = DeliveryPolicy::Latest;
|
||||
DeliveryPolicy scroll = DeliveryPolicy::Accumulate;
|
||||
};
|
||||
|
||||
struct WindowConfigure {
|
||||
int32_t width = 0;
|
||||
int32_t height = 0;
|
||||
bool maximized = false;
|
||||
bool fullscreen = false;
|
||||
bool activated = false;
|
||||
bool tiled = false;
|
||||
uint32_t serial = 0;
|
||||
};
|
||||
|
||||
struct CoalescingStatistics {
|
||||
uint64_t received = 0;
|
||||
uint64_t delivered = 0;
|
||||
uint64_t configuresReceived = 0;
|
||||
uint64_t configuresApplied = 0;
|
||||
};
|
||||
|
||||
// Backends push, this class batches, delivers once per frame with new abstraction types
|
||||
class InputCoalescer {
|
||||
public:
|
||||
InputCoalescer();
|
||||
~InputCoalescer();
|
||||
|
||||
InputCoalescer(const InputCoalescer&) = delete;
|
||||
InputCoalescer& operator=(const InputCoalescer&) = delete;
|
||||
|
||||
[[nodiscard]] InputEventSink& recorder();
|
||||
void setTap(InputEventSink* tap) { mTap = tap; }
|
||||
[[nodiscard]] InputEventSink* tap() const { return mTap; }
|
||||
|
||||
void setPolicies(const InputDeliveryPolicies& policies) { mPolicies = policies; }
|
||||
[[nodiscard]] const InputDeliveryPolicies& policies() const { return mPolicies; }
|
||||
|
||||
void pointerSample(const PointerSample& sample) { mSample = sample; }
|
||||
void pointerMoved(Vec2F position);
|
||||
void pointerLeft();
|
||||
void pointerButton(InputID button, bool pressed);
|
||||
void scrolled(Vec2F delta, ScrollSource source = ScrollSource::Wheel);
|
||||
void scrollEnded();
|
||||
void key(InputID key, bool pressed);
|
||||
void keyRepeat(InputID key);
|
||||
void text(const std::string& utf8);
|
||||
void keyboardFocus(bool focused);
|
||||
void gesture(const TouchGesture& gesture);
|
||||
|
||||
void configure(const WindowConfigure& configure);
|
||||
[[nodiscard]] bool hasPendingConfigure() const { return mHasConfigure; }
|
||||
[[nodiscard]] WindowConfigure takeConfigure();
|
||||
|
||||
[[nodiscard]] bool isEmpty() const { return mEvents.empty(); }
|
||||
[[nodiscard]] size_t pendingEvents() const { return mEvents.size(); }
|
||||
void deliver(InputEventSink& observer);
|
||||
|
||||
[[nodiscard]] const CoalescingStatistics& statistics() const { return mStatistics; }
|
||||
|
||||
private:
|
||||
enum class Kind { Motion, Leave, Button, Scroll, ScrollEnd, Key, Repeat, Text, Focus, Gesture };
|
||||
|
||||
struct Pending {
|
||||
Kind kind;
|
||||
Vec2F vector = { 0, 0 };
|
||||
InputID id = InputID::SPACE;
|
||||
bool flag = false;
|
||||
std::string text;
|
||||
PointerSample sample;
|
||||
TouchGesture gesture;
|
||||
};
|
||||
|
||||
void push(Pending event, DeliveryPolicy policy);
|
||||
|
||||
class Recorder;
|
||||
|
||||
std::unique_ptr<Recorder> mRecorder;
|
||||
InputEventSink* mTap = nullptr;
|
||||
InputDeliveryPolicies mPolicies;
|
||||
std::vector<Pending> mEvents;
|
||||
PointerSample mSample;
|
||||
PointerSample mDeliveredSample;
|
||||
WindowConfigure mConfigure;
|
||||
bool mHasConfigure = false;
|
||||
CoalescingStatistics mStatistics;
|
||||
};
|
||||
}
|
||||
|
|
@ -5,13 +5,13 @@
|
|||
|
||||
namespace tp {
|
||||
enum class InputID : ualni {
|
||||
/* Printable keys */
|
||||
NONE = 0,
|
||||
SPACE = 32,
|
||||
APOSTROPHE = 39, /* ' */
|
||||
COMMA = 44, /* , */
|
||||
MINUS = 45, /* - */
|
||||
PERIOD = 46, /* . */
|
||||
SLASH = 0xBF, /* \ */
|
||||
APOSTROPHE = 39,
|
||||
COMMA = 44,
|
||||
MINUS = 45,
|
||||
PERIOD = 46,
|
||||
SLASH = 0xBF,
|
||||
N0 = 48,
|
||||
N1 = 49,
|
||||
N2 = 50,
|
||||
|
|
@ -22,8 +22,8 @@ namespace tp {
|
|||
N7 = 55,
|
||||
N8 = 56,
|
||||
N9 = 57,
|
||||
SEMICOLON = 59, /* ; */
|
||||
EQUAL = 61, /* = */
|
||||
SEMICOLON = 59,
|
||||
EQUAL = 61,
|
||||
A = 65,
|
||||
B = 66,
|
||||
C = 67,
|
||||
|
|
@ -50,14 +50,12 @@ namespace tp {
|
|||
X = 88,
|
||||
Y = 89,
|
||||
Z = 90,
|
||||
LEFT_BRACKET = 91, /* [ */
|
||||
BACKSLASH = 92, /* \ */
|
||||
RIGHT_BRACKET = 93, /* ] */
|
||||
GRAVE_ACCENT = 96, /* ` */
|
||||
WORLD_1 = 161, /* non-US #1 */
|
||||
WORLD_2 = 162, /* non-US #2 */
|
||||
|
||||
/* function keys */
|
||||
LEFT_BRACKET = 91,
|
||||
BACKSLASH = 92,
|
||||
RIGHT_BRACKET = 93,
|
||||
GRAVE_ACCENT = 96,
|
||||
WORLD_1 = 161,
|
||||
WORLD_2 = 162,
|
||||
|
||||
ESCAPE = 256,
|
||||
ENTER = 257,
|
||||
32
GraphicApplication/src/input_layer/public/InputEventSink.hpp
Normal file
32
GraphicApplication/src/input_layer/public/InputEventSink.hpp
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputCodes.hpp"
|
||||
#include "PointerSample.hpp"
|
||||
#include "TouchGesture.hpp"
|
||||
#include "Vec.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace tp {
|
||||
|
||||
enum class ScrollSource { Wheel, Touchpad };
|
||||
|
||||
inline constexpr halnf kPointerOutsideCoordinate = -1.0e5f;
|
||||
|
||||
class InputEventSink {
|
||||
public:
|
||||
virtual ~InputEventSink() = default;
|
||||
|
||||
virtual void onPointerSample(const PointerSample& sample) {}
|
||||
virtual void onPointerMoved(Vec2F contentPosition) {}
|
||||
virtual void onPointerLeft() {}
|
||||
virtual void onPointerButton(InputID button, bool pressed) {}
|
||||
virtual void onScrolled(Vec2F delta, ScrollSource source) {}
|
||||
virtual void onScrollEnded() {}
|
||||
virtual void onKey(InputID key, bool pressed) {}
|
||||
virtual void onKeyRepeat(InputID key) {}
|
||||
virtual void onText(const std::string& utf8) {}
|
||||
virtual void onKeyboardFocus(bool focused) {}
|
||||
virtual void onGesture(const TouchGesture& gesture) {}
|
||||
};
|
||||
}
|
||||
69
GraphicApplication/src/input_layer/public/InputRecording.hpp
Normal file
69
GraphicApplication/src/input_layer/public/InputRecording.hpp
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputEventSink.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
struct RecordedInput {
|
||||
enum class Kind { Sample, Move, Leave, Button, Scroll, Key, Focus, TouchpadScroll, ScrollEnd };
|
||||
|
||||
double timeMs = 0;
|
||||
Kind kind = Kind::Move;
|
||||
Vec2F vector = { 0, 0 };
|
||||
InputID id = InputID::MOUSE1;
|
||||
bool flag = false;
|
||||
PointerSample sample;
|
||||
|
||||
void deliverTo(InputEventSink& observer) const;
|
||||
[[nodiscard]] bool operator==(const RecordedInput& other) const = default;
|
||||
};
|
||||
|
||||
struct InputRecording {
|
||||
static constexpr const char* kHeader = "tp-input-recording 1";
|
||||
|
||||
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);
|
||||
|
||||
[[nodiscard]] InputRecording translated(Vec2F offset) const;
|
||||
[[nodiscard]] InputRecording slicedToPointerActivity() const;
|
||||
};
|
||||
|
||||
class InputRecorder : public InputEventSink {
|
||||
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 onPointerSample(const PointerSample& sample) override;
|
||||
void onPointerMoved(Vec2F contentPosition) override;
|
||||
void onPointerLeft() override;
|
||||
void onPointerButton(InputID button, bool pressed) override;
|
||||
void onScrolled(Vec2F delta, ScrollSource source) override;
|
||||
void onScrollEnded() override;
|
||||
void onKey(InputID key, bool pressed) override;
|
||||
void onKeyboardFocus(bool focused) override;
|
||||
|
||||
private:
|
||||
void record(RecordedInput event);
|
||||
|
||||
Clock mClock;
|
||||
double mStartMs = -1;
|
||||
InputRecording mRecording;
|
||||
};
|
||||
}
|
||||
115
GraphicApplication/src/input_layer/public/InputState.hpp
Normal file
115
GraphicApplication/src/input_layer/public/InputState.hpp
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputEventSink.hpp"
|
||||
#include "Vec.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
struct InputEvent {
|
||||
enum class Type {
|
||||
NONE = 0,
|
||||
BUTTON_ACTION,
|
||||
MOUSE_DELTA,
|
||||
MOUSE_POS,
|
||||
SCROLL,
|
||||
SCROLL_END,
|
||||
POINTER_SAMPLE,
|
||||
TEXT,
|
||||
GESTURE,
|
||||
FOCUS,
|
||||
RESIZED,
|
||||
} type = Type::NONE;
|
||||
|
||||
enum class ButtonAction {
|
||||
NONE = 0,
|
||||
PRESS,
|
||||
HOLD,
|
||||
RELEASE,
|
||||
REPEAT,
|
||||
} buttonAction = ButtonAction::NONE;
|
||||
|
||||
Vec2F mouseEvent = { 0, 0 };
|
||||
Vec2F scrollDelta = { 0, 0 };
|
||||
PointerSample sample;
|
||||
bool continuousScroll = false;
|
||||
std::string text;
|
||||
TouchGesture gesture;
|
||||
bool focused = false;
|
||||
InputID id = InputID::NONE;
|
||||
};
|
||||
|
||||
struct ButtonState {
|
||||
bool pressed = false;
|
||||
bool down = false;
|
||||
bool released = false;
|
||||
};
|
||||
|
||||
class InputState : public InputEventSink {
|
||||
public:
|
||||
void onPointerSample(const PointerSample& sample) override;
|
||||
void onPointerMoved(Vec2F contentPosition) override;
|
||||
void onPointerLeft() override;
|
||||
void onPointerButton(InputID button, bool pressed) override;
|
||||
void onScrolled(Vec2F delta, ScrollSource source) override;
|
||||
void onScrollEnded() override;
|
||||
void onKey(InputID key, bool pressed) override;
|
||||
void onKeyRepeat(InputID key) override;
|
||||
void onText(const std::string& utf8) override;
|
||||
void onKeyboardFocus(bool focused) override;
|
||||
void onGesture(const TouchGesture& gesture) override;
|
||||
void onResized();
|
||||
|
||||
void postEvent(InputID inputID, InputEvent inputEvent);
|
||||
|
||||
[[nodiscard]] size_t pendingEventCount() const { return mPendingEvents.size(); }
|
||||
void applyPendingEvents();
|
||||
|
||||
[[nodiscard]] const std::vector<InputEvent>& events() const { return mFrameEvents; }
|
||||
|
||||
void setCursorOrigin(const Vec2F& origin);
|
||||
|
||||
[[nodiscard]] Vec2F getPointer() const;
|
||||
[[nodiscard]] Vec2F getPointerPrev() const;
|
||||
[[nodiscard]] Vec2F getPointerDelta() const;
|
||||
|
||||
[[nodiscard]] bool isPressed(InputID id) const;
|
||||
[[nodiscard]] bool isReleased(InputID id) const;
|
||||
[[nodiscard]] bool isDown(InputID id) const;
|
||||
[[nodiscard]] halnf getScrollY() const;
|
||||
[[nodiscard]] halnf getScrollX() const;
|
||||
[[nodiscard]] bool isContinuousScroll() const { return mContinuousScroll; }
|
||||
[[nodiscard]] bool isScrollEnded() const { return mScrollEnded; }
|
||||
|
||||
[[nodiscard]] halnf getPointerPressure() const;
|
||||
[[nodiscard]] const PointerSample& getPointerSample() const { return mPointerSample; }
|
||||
|
||||
void setEnableKeyEvents(bool);
|
||||
|
||||
private:
|
||||
void postButton(InputID id, InputEvent::ButtonAction action);
|
||||
void releaseHeldButtons();
|
||||
void applyEvent(const InputEvent& event);
|
||||
|
||||
std::vector<InputEvent> mPendingEvents;
|
||||
std::vector<InputEvent> mFrameEvents;
|
||||
std::set<InputID> mHeldKeys;
|
||||
std::set<InputID> mHeldPointerButtons;
|
||||
|
||||
Vec2F mPointerOrigin = { 0, 0 };
|
||||
Vec2F mPointer;
|
||||
Vec2F mPointerPrev;
|
||||
Vec2F mScrollDelta;
|
||||
bool mContinuousScroll = false;
|
||||
bool mScrollEnded = false;
|
||||
|
||||
PointerSample mPointerSample;
|
||||
ButtonState mButtonStates[(int) InputID::LAST_KEY_CODE]{};
|
||||
|
||||
bool mEnableKeyEvents = true;
|
||||
};
|
||||
}
|
||||
13
GraphicApplication/src/input_layer/public/KeyMapping.hpp
Normal file
13
GraphicApplication/src/input_layer/public/KeyMapping.hpp
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputCodes.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace tp {
|
||||
|
||||
[[nodiscard]] std::optional<InputID> inputFromEvdevKey(uint32_t evdevKey);
|
||||
|
||||
[[nodiscard]] std::optional<InputID> inputFromPointerButton(uint32_t linuxButton);
|
||||
}
|
||||
25
GraphicApplication/src/input_layer/public/PointerSample.hpp
Normal file
25
GraphicApplication/src/input_layer/public/PointerSample.hpp
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include "Vec.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace tp {
|
||||
|
||||
enum class PointerDevice { Mouse, Finger, Stylus };
|
||||
|
||||
enum class StylusTip { Pen, Eraser, Brush, Pencil, Airbrush };
|
||||
|
||||
struct PointerSample {
|
||||
PointerDevice device = PointerDevice::Mouse;
|
||||
StylusTip tip = StylusTip::Pen;
|
||||
halnf pressure = 1;
|
||||
Vec2F tilt = { 0, 0 };
|
||||
halnf rotation = 0;
|
||||
halnf distance = 0;
|
||||
uint32_t timeMs = 0;
|
||||
|
||||
[[nodiscard]] bool isStylus() const { return device == PointerDevice::Stylus; }
|
||||
[[nodiscard]] bool operator==(const PointerSample& other) const = default;
|
||||
};
|
||||
}
|
||||
70
GraphicApplication/src/input_layer/public/TouchGesture.hpp
Normal file
70
GraphicApplication/src/input_layer/public/TouchGesture.hpp
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
#pragma once
|
||||
|
||||
#include "Vec.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
enum class GesturePhase { Begin, Update, End };
|
||||
|
||||
struct TouchGesture {
|
||||
GesturePhase phase = GesturePhase::Update;
|
||||
Vec2F center = { 0, 0 };
|
||||
Vec2F pan = { 0, 0 };
|
||||
halnf scale = 1;
|
||||
halnf rotation = 0;
|
||||
int32_t tapFingers = 0;
|
||||
|
||||
[[nodiscard]] bool operator==(const TouchGesture& other) const = default;
|
||||
};
|
||||
|
||||
struct TouchPoint {
|
||||
int32_t id = 0;
|
||||
Vec2F position = { 0, 0 };
|
||||
};
|
||||
|
||||
class TouchGestureTracker {
|
||||
public:
|
||||
static constexpr uint32_t kTapMaximumMs = 300;
|
||||
static constexpr halnf kTapMaximumTravel = 20;
|
||||
|
||||
void update(const std::vector<TouchPoint>& touches, std::vector<TouchGesture>& gestures, uint32_t timeMs = 0);
|
||||
void cancel(std::vector<TouchGesture>& gestures);
|
||||
|
||||
[[nodiscard]] bool isActive() const { return mActive; }
|
||||
|
||||
private:
|
||||
void track(const TouchPoint& first, const TouchPoint& second);
|
||||
void followTap(const std::vector<TouchPoint>& touches, uint32_t timeMs);
|
||||
[[nodiscard]] int32_t finishedTapFingers(uint32_t timeMs) const;
|
||||
|
||||
std::vector<TouchPoint> mTapStarts;
|
||||
uint32_t mTapStartMs = 0;
|
||||
bool mTapStillPossible = false;
|
||||
|
||||
bool mActive = false;
|
||||
int32_t mFirst = 0;
|
||||
int32_t mSecond = 0;
|
||||
Vec2F mCenter = { 0, 0 };
|
||||
halnf mDistance = 0;
|
||||
halnf mAngle = 0;
|
||||
};
|
||||
|
||||
void accumulate(TouchGesture& into, const TouchGesture& next);
|
||||
|
||||
// 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;
|
||||
};
|
||||
}
|
||||
94
GraphicApplication/src/render/private/GpuTimer.cpp
Normal file
94
GraphicApplication/src/render/private/GpuTimer.cpp
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#include "GpuTimer.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
GpuTimer::GpuTimer(VkDevice device, VkPhysicalDevice physicalDevice, uint32_t queueFamily, const char* name) :
|
||||
mDevice(device),
|
||||
mSection(Profiler::global().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() || !Profiler::global().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;
|
||||
mLastMs = (double) ticks * mTickNs / 1.0e6;
|
||||
Profiler::global().addSectionTime(mSection, mLastMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
828
GraphicApplication/src/render/private/VulkanContext.cpp
Normal file
828
GraphicApplication/src/render/private/VulkanContext.cpp
Normal file
|
|
@ -0,0 +1,828 @@
|
|||
#include <cstdio>
|
||||
#include "VulkanContext.hpp"
|
||||
#include "GpuTimer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace tp {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array<const char*, 1> kDeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
|
||||
|
||||
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 auto& layer : layers) {
|
||||
if (std::strcmp(layer.layerName, "VK_LAYER_KHRONOS_validation") == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool deviceSupportsExtensions(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) {
|
||||
bool found = false;
|
||||
for (const auto& extension : available) {
|
||||
if (std::strcmp(required, extension.extensionName) == 0) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
VulkanContext::VulkanContext(VulkanSurfaceSource& surfaceSource) :
|
||||
mSurfaceSource(surfaceSource) {
|
||||
createInstance();
|
||||
createSurface();
|
||||
selectPhysicalDevice();
|
||||
selectCompositeAlpha();
|
||||
createLogicalDevice();
|
||||
createRenderPass();
|
||||
createCommandResources();
|
||||
createSyncObjects();
|
||||
mFrameGpuTimer = std::make_unique<GpuTimer>(mDevice, mPhysicalDevice, mGraphicsQueueFamily, "gpu.window");
|
||||
|
||||
const VkExtent2D extent = mSurfaceSource.framebufferExtent();
|
||||
createSwapchain(std::max<int>((int) extent.width, 1), std::max<int>((int) extent.height, 1));
|
||||
mRequestedExtent = extent;
|
||||
}
|
||||
|
||||
VulkanContext::~VulkanContext() {
|
||||
if (mDevice != VK_NULL_HANDLE) {
|
||||
vkDeviceWaitIdle(mDevice);
|
||||
}
|
||||
mFrameGpuTimer.reset();
|
||||
|
||||
destroySwapchain();
|
||||
|
||||
for (auto semaphore : mImageAvailableSemaphores) {
|
||||
vkDestroySemaphore(mDevice, semaphore, nullptr);
|
||||
}
|
||||
for (auto semaphore : mRenderFinishedSemaphores) {
|
||||
vkDestroySemaphore(mDevice, semaphore, nullptr);
|
||||
}
|
||||
for (auto fence : mInFlightFences) {
|
||||
vkDestroyFence(mDevice, fence, nullptr);
|
||||
}
|
||||
|
||||
if (mCommandPool != VK_NULL_HANDLE) {
|
||||
vkDestroyCommandPool(mDevice, mCommandPool, nullptr);
|
||||
}
|
||||
if (mRenderPass != VK_NULL_HANDLE) {
|
||||
vkDestroyRenderPass(mDevice, mRenderPass, nullptr);
|
||||
}
|
||||
if (mDevice != VK_NULL_HANDLE) {
|
||||
vkDestroyDevice(mDevice, nullptr);
|
||||
}
|
||||
if (mSurface != VK_NULL_HANDLE) {
|
||||
vkDestroySurfaceKHR(mInstance, mSurface, nullptr);
|
||||
}
|
||||
if (mInstance != VK_NULL_HANDLE) {
|
||||
vkDestroyInstance(mInstance, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanContext::createInstance() {
|
||||
std::vector<const char*> extensions = mSurfaceSource.requiredInstanceExtensions();
|
||||
|
||||
std::vector<const char*> layers;
|
||||
if (validationRequested() && hasValidationLayer()) {
|
||||
layers.push_back("VK_LAYER_KHRONOS_validation");
|
||||
}
|
||||
|
||||
VkApplicationInfo appInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
|
||||
.pApplicationName = "Modules",
|
||||
.apiVersion = VK_API_VERSION_1_2,
|
||||
};
|
||||
|
||||
VkInstanceCreateInfo createInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
|
||||
.pApplicationInfo = &appInfo,
|
||||
.enabledLayerCount = static_cast<uint32_t>(layers.size()),
|
||||
.ppEnabledLayerNames = layers.data(),
|
||||
.enabledExtensionCount = static_cast<uint32_t>(extensions.size()),
|
||||
.ppEnabledExtensionNames = extensions.data(),
|
||||
};
|
||||
|
||||
if (vkCreateInstance(&createInfo, nullptr, &mInstance) != VK_SUCCESS) {
|
||||
throw std::runtime_error("failed to create vulkan instance");
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanContext::createSurface() {
|
||||
mSurface = mSurfaceSource.createSurface(mInstance);
|
||||
if (mSurface == VK_NULL_HANDLE) {
|
||||
throw std::runtime_error("failed to create window surface");
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanContext::selectCompositeAlpha() {
|
||||
VkSurfaceCapabilitiesKHR capabilities{};
|
||||
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(mPhysicalDevice, mSurface, &capabilities);
|
||||
|
||||
mCompositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
|
||||
if (!(capabilities.supportedCompositeAlpha & 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 (capabilities.supportedCompositeAlpha & candidate) {
|
||||
mCompositeAlpha = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
mSurfaceSource.wantsTranslucency() &&
|
||||
(capabilities.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR)
|
||||
) {
|
||||
mCompositeAlpha = VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR;
|
||||
mTranslucent = true;
|
||||
}
|
||||
|
||||
mSurfaceSource.onTranslucencyResolved(isTranslucent());
|
||||
}
|
||||
|
||||
void VulkanContext::selectPhysicalDevice() {
|
||||
uint32_t deviceCount = 0;
|
||||
vkEnumeratePhysicalDevices(mInstance, &deviceCount, nullptr);
|
||||
if (deviceCount == 0) {
|
||||
throw std::runtime_error("no vulkan capable gpu found");
|
||||
}
|
||||
|
||||
std::vector<VkPhysicalDevice> devices(deviceCount);
|
||||
vkEnumeratePhysicalDevices(mInstance, &deviceCount, devices.data());
|
||||
|
||||
for (auto device : devices) {
|
||||
if (!deviceSupportsExtensions(device)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t familyCount = 0;
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(device, &familyCount, nullptr);
|
||||
std::vector<VkQueueFamilyProperties> families(familyCount);
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(device, &familyCount, families.data());
|
||||
|
||||
uint32_t graphics = UINT32_MAX;
|
||||
uint32_t present = UINT32_MAX;
|
||||
for (uint32_t i = 0; i < familyCount; i++) {
|
||||
if (families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
|
||||
graphics = i;
|
||||
}
|
||||
VkBool32 presentSupport = VK_FALSE;
|
||||
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, mSurface, &presentSupport);
|
||||
if (presentSupport) {
|
||||
present = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (graphics == UINT32_MAX || present == UINT32_MAX) {
|
||||
continue;
|
||||
}
|
||||
|
||||
mPhysicalDevice = device;
|
||||
mGraphicsQueueFamily = graphics;
|
||||
mPresentQueueFamily = present;
|
||||
return;
|
||||
}
|
||||
|
||||
throw std::runtime_error("no suitable vulkan device");
|
||||
}
|
||||
|
||||
void VulkanContext::createLogicalDevice() {
|
||||
const float priority = 1.0f;
|
||||
std::vector<VkDeviceQueueCreateInfo> queueInfos;
|
||||
|
||||
VkDeviceQueueCreateInfo graphicsInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
|
||||
.queueFamilyIndex = mGraphicsQueueFamily,
|
||||
.queueCount = 1,
|
||||
.pQueuePriorities = &priority,
|
||||
};
|
||||
queueInfos.push_back(graphicsInfo);
|
||||
|
||||
if (mPresentQueueFamily != mGraphicsQueueFamily) {
|
||||
VkDeviceQueueCreateInfo presentInfo = graphicsInfo;
|
||||
presentInfo.queueFamilyIndex = mPresentQueueFamily;
|
||||
queueInfos.push_back(presentInfo);
|
||||
}
|
||||
|
||||
VkPhysicalDeviceFeatures supported{};
|
||||
vkGetPhysicalDeviceFeatures(mPhysicalDevice, &supported);
|
||||
VkPhysicalDeviceFeatures features{};
|
||||
features.geometryShader = supported.geometryShader;
|
||||
|
||||
VkDeviceCreateInfo createInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
|
||||
.queueCreateInfoCount = static_cast<uint32_t>(queueInfos.size()),
|
||||
.pQueueCreateInfos = queueInfos.data(),
|
||||
.enabledExtensionCount = static_cast<uint32_t>(kDeviceExtensions.size()),
|
||||
.ppEnabledExtensionNames = kDeviceExtensions.data(),
|
||||
.pEnabledFeatures = &features,
|
||||
};
|
||||
|
||||
if (vkCreateDevice(mPhysicalDevice, &createInfo, nullptr, &mDevice) != VK_SUCCESS) {
|
||||
throw std::runtime_error("failed to create logical device");
|
||||
}
|
||||
|
||||
vkGetDeviceQueue(mDevice, mGraphicsQueueFamily, 0, &mGraphicsQueue);
|
||||
vkGetDeviceQueue(mDevice, mPresentQueueFamily, 0, &mPresentQueue);
|
||||
}
|
||||
|
||||
void VulkanContext::createRenderPass() {
|
||||
uint32_t formatCount = 0;
|
||||
vkGetPhysicalDeviceSurfaceFormatsKHR(mPhysicalDevice, mSurface, &formatCount, nullptr);
|
||||
std::vector<VkSurfaceFormatKHR> formats(formatCount);
|
||||
vkGetPhysicalDeviceSurfaceFormatsKHR(mPhysicalDevice, mSurface, &formatCount, formats.data());
|
||||
|
||||
mSwapchainFormat = formats.front().format;
|
||||
for (const auto& 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,
|
||||
},
|
||||
};
|
||||
|
||||
VkAttachmentReference colorRef{
|
||||
.attachment = 0,
|
||||
.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
|
||||
};
|
||||
|
||||
VkAttachmentReference depthRef{
|
||||
.attachment = 1,
|
||||
.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
|
||||
};
|
||||
|
||||
VkSubpassDescription subpass{
|
||||
.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
.colorAttachmentCount = 1,
|
||||
.pColorAttachments = &colorRef,
|
||||
.pDepthStencilAttachment = &depthRef,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
VkRenderPassCreateInfo createInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
|
||||
.attachmentCount = static_cast<uint32_t>(attachments.size()),
|
||||
.pAttachments = attachments.data(),
|
||||
.subpassCount = 1,
|
||||
.pSubpasses = &subpass,
|
||||
.dependencyCount = 1,
|
||||
.pDependencies = &dependency,
|
||||
};
|
||||
|
||||
if (vkCreateRenderPass(mDevice, &createInfo, nullptr, &mRenderPass) != VK_SUCCESS) {
|
||||
throw std::runtime_error("failed to create render pass");
|
||||
}
|
||||
}
|
||||
|
||||
VkFormat VulkanContext::selectDepthStencilFormat() const {
|
||||
const std::array<VkFormat, 3> candidates = {
|
||||
VK_FORMAT_D24_UNORM_S8_UINT,
|
||||
VK_FORMAT_D32_SFLOAT_S8_UINT,
|
||||
VK_FORMAT_D16_UNORM_S8_UINT,
|
||||
};
|
||||
for (VkFormat format : candidates) {
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(mPhysicalDevice, format, &properties);
|
||||
if (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) {
|
||||
return format;
|
||||
}
|
||||
}
|
||||
throw std::runtime_error("no supported depth-stencil format");
|
||||
}
|
||||
|
||||
uint32_t VulkanContext::findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) const {
|
||||
VkPhysicalDeviceMemoryProperties memoryProperties{};
|
||||
vkGetPhysicalDeviceMemoryProperties(mPhysicalDevice, &memoryProperties);
|
||||
for (uint32_t i = 0; i < memoryProperties.memoryTypeCount; i++) {
|
||||
if ((typeFilter & (1 << i)) && (memoryProperties.memoryTypes[i].propertyFlags & properties) == properties) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
throw std::runtime_error("no suitable memory type for depth-stencil");
|
||||
}
|
||||
|
||||
void VulkanContext::createDepthStencil() {
|
||||
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,
|
||||
};
|
||||
if (vkCreateImage(mDevice, &imageInfo, nullptr, &mDepthStencilImage) != VK_SUCCESS) {
|
||||
throw std::runtime_error("failed to create depth-stencil image");
|
||||
}
|
||||
|
||||
VkMemoryRequirements requirements{};
|
||||
vkGetImageMemoryRequirements(mDevice, mDepthStencilImage, &requirements);
|
||||
|
||||
VkMemoryAllocateInfo allocateInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
|
||||
.allocationSize = requirements.size,
|
||||
.memoryTypeIndex = findMemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT),
|
||||
};
|
||||
if (vkAllocateMemory(mDevice, &allocateInfo, nullptr, &mDepthStencilMemory) != VK_SUCCESS) {
|
||||
throw std::runtime_error("failed to allocate depth-stencil memory");
|
||||
}
|
||||
vkBindImageMemory(mDevice, mDepthStencilImage, mDepthStencilMemory, 0);
|
||||
|
||||
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 },
|
||||
};
|
||||
if (vkCreateImageView(mDevice, &viewInfo, nullptr, &mDepthStencilView) != VK_SUCCESS) {
|
||||
throw std::runtime_error("failed to create depth-stencil view");
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanContext::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 VulkanContext::createCommandResources() {
|
||||
VkCommandPoolCreateInfo poolInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
|
||||
.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
|
||||
.queueFamilyIndex = mGraphicsQueueFamily,
|
||||
};
|
||||
if (vkCreateCommandPool(mDevice, &poolInfo, nullptr, &mCommandPool) != VK_SUCCESS) {
|
||||
throw std::runtime_error("failed to create command pool");
|
||||
}
|
||||
|
||||
mCommandBuffers.resize(kFramesInFlight);
|
||||
VkCommandBufferAllocateInfo allocateInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
|
||||
.commandPool = mCommandPool,
|
||||
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
|
||||
.commandBufferCount = kFramesInFlight,
|
||||
};
|
||||
if (vkAllocateCommandBuffers(mDevice, &allocateInfo, mCommandBuffers.data()) != VK_SUCCESS) {
|
||||
throw std::runtime_error("failed to allocate command buffers");
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanContext::createSyncObjects() {
|
||||
mImageAvailableSemaphores.resize(kFramesInFlight);
|
||||
mRenderFinishedSemaphores.resize(kFramesInFlight);
|
||||
mInFlightFences.resize(kFramesInFlight);
|
||||
|
||||
VkSemaphoreCreateInfo semaphoreInfo{ .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
|
||||
VkFenceCreateInfo fenceInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
|
||||
.flags = VK_FENCE_CREATE_SIGNALED_BIT,
|
||||
};
|
||||
for (uint32_t i = 0; i < kFramesInFlight; i++) {
|
||||
vkCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &mImageAvailableSemaphores[i]);
|
||||
vkCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &mRenderFinishedSemaphores[i]);
|
||||
vkCreateFence(mDevice, &fenceInfo, nullptr, &mInFlightFences[i]);
|
||||
}
|
||||
}
|
||||
|
||||
VkPresentModeKHR VulkanContext::selectPresentMode() const {
|
||||
uint32_t count = 0;
|
||||
vkGetPhysicalDeviceSurfacePresentModesKHR(mPhysicalDevice, mSurface, &count, nullptr);
|
||||
std::vector<VkPresentModeKHR> modes(count);
|
||||
vkGetPhysicalDeviceSurfacePresentModesKHR(mPhysicalDevice, mSurface, &count, modes.data());
|
||||
|
||||
// Mailbox replaces the queued frame instead of waiting behind it, so the
|
||||
// compositor always shows the newest frame. FIFO is the guaranteed fallback.
|
||||
for (VkPresentModeKHR mode : modes) {
|
||||
if (mode == VK_PRESENT_MODE_MAILBOX_KHR) {
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
return VK_PRESENT_MODE_FIFO_KHR;
|
||||
}
|
||||
|
||||
void VulkanContext::createSwapchain(int width, int height, VkSwapchainKHR previous) {
|
||||
VkSurfaceCapabilitiesKHR capabilities{};
|
||||
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(mPhysicalDevice, mSurface, &capabilities);
|
||||
|
||||
mSwapchainExtent = capabilities.currentExtent;
|
||||
mSurfaceTransform = capabilities.currentTransform;
|
||||
mSurfaceExtent = capabilities.currentExtent;
|
||||
VkSurfaceTransformFlagBitsKHR transform = capabilities.currentTransform;
|
||||
if (
|
||||
transform != VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR &&
|
||||
(capabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR)
|
||||
) {
|
||||
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 != std::numeric_limits<uint32_t>::max()) {
|
||||
std::swap(mSwapchainExtent.width, mSwapchainExtent.height);
|
||||
}
|
||||
}
|
||||
if (mSwapchainExtent.width == std::numeric_limits<uint32_t>::max()) {
|
||||
mSwapchainExtent.width =
|
||||
std::clamp(static_cast<uint32_t>(width), capabilities.minImageExtent.width, capabilities.maxImageExtent.width);
|
||||
mSwapchainExtent.height = std::clamp(
|
||||
static_cast<uint32_t>(height), capabilities.minImageExtent.height, capabilities.maxImageExtent.height
|
||||
);
|
||||
}
|
||||
|
||||
mPresentMode = selectPresentMode();
|
||||
uint32_t imageCount = capabilities.minImageCount;
|
||||
if (mPresentMode == VK_PRESENT_MODE_MAILBOX_KHR) {
|
||||
imageCount += 1;
|
||||
}
|
||||
if (capabilities.maxImageCount > 0 && imageCount > capabilities.maxImageCount) {
|
||||
imageCount = capabilities.maxImageCount;
|
||||
}
|
||||
|
||||
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,
|
||||
.preTransform = transform,
|
||||
.compositeAlpha = mCompositeAlpha,
|
||||
.presentMode = mPresentMode,
|
||||
.clipped = VK_TRUE,
|
||||
.oldSwapchain = previous,
|
||||
};
|
||||
|
||||
const uint32_t families[] = { mGraphicsQueueFamily, mPresentQueueFamily };
|
||||
if (mGraphicsQueueFamily != mPresentQueueFamily) {
|
||||
createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
|
||||
createInfo.queueFamilyIndexCount = 2;
|
||||
createInfo.pQueueFamilyIndices = families;
|
||||
} else {
|
||||
createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
}
|
||||
|
||||
if (vkCreateSwapchainKHR(mDevice, &createInfo, nullptr, &mSwapchain) != VK_SUCCESS) {
|
||||
throw std::runtime_error("failed to create swapchain");
|
||||
}
|
||||
if (previous != VK_NULL_HANDLE) {
|
||||
vkDestroySwapchainKHR(mDevice, previous, nullptr);
|
||||
}
|
||||
|
||||
uint32_t actualImageCount = 0;
|
||||
vkGetSwapchainImagesKHR(mDevice, mSwapchain, &actualImageCount, nullptr);
|
||||
mSwapchainImages.resize(actualImageCount);
|
||||
vkGetSwapchainImagesKHR(mDevice, mSwapchain, &actualImageCount, mSwapchainImages.data());
|
||||
|
||||
createDepthStencil();
|
||||
|
||||
mSwapchainImageViews.resize(actualImageCount);
|
||||
mSwapchainFramebuffers.resize(actualImageCount);
|
||||
for (uint32_t i = 0; i < actualImageCount; i++) {
|
||||
VkImageViewCreateInfo viewInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||
.image = mSwapchainImages[i],
|
||||
.viewType = VK_IMAGE_VIEW_TYPE_2D,
|
||||
.format = mSwapchainFormat,
|
||||
.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 },
|
||||
};
|
||||
if (vkCreateImageView(mDevice, &viewInfo, nullptr, &mSwapchainImageViews[i]) != VK_SUCCESS) {
|
||||
throw std::runtime_error("failed to create swapchain image view");
|
||||
}
|
||||
|
||||
const std::array<VkImageView, 2> framebufferAttachments = {
|
||||
mSwapchainImageViews[i],
|
||||
mDepthStencilView,
|
||||
};
|
||||
|
||||
VkFramebufferCreateInfo framebufferInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
|
||||
.renderPass = mRenderPass,
|
||||
.attachmentCount = static_cast<uint32_t>(framebufferAttachments.size()),
|
||||
.pAttachments = framebufferAttachments.data(),
|
||||
.width = mSwapchainExtent.width,
|
||||
.height = mSwapchainExtent.height,
|
||||
.layers = 1,
|
||||
};
|
||||
if (vkCreateFramebuffer(mDevice, &framebufferInfo, nullptr, &mSwapchainFramebuffers[i]) != VK_SUCCESS) {
|
||||
throw std::runtime_error("failed to create framebuffer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanContext::destroySwapchainResources() {
|
||||
destroyDepthStencil();
|
||||
for (auto framebuffer : mSwapchainFramebuffers) {
|
||||
vkDestroyFramebuffer(mDevice, framebuffer, nullptr);
|
||||
}
|
||||
for (auto view : mSwapchainImageViews) {
|
||||
vkDestroyImageView(mDevice, view, nullptr);
|
||||
}
|
||||
|
||||
mSwapchainFramebuffers.clear();
|
||||
mSwapchainImageViews.clear();
|
||||
mSwapchainImages.clear();
|
||||
}
|
||||
|
||||
void VulkanContext::destroySwapchain() {
|
||||
destroySwapchainResources();
|
||||
if (mSwapchain != VK_NULL_HANDLE) {
|
||||
vkDestroySwapchainKHR(mDevice, mSwapchain, nullptr);
|
||||
}
|
||||
mSwapchain = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
void VulkanContext::recreateSwapchain(int width, int height) {
|
||||
// Only the frames using the old swapchain's views must finish; the old
|
||||
// swapchain is handed to the new one so presentation never stalls on a gap.
|
||||
vkWaitForFences(
|
||||
mDevice, static_cast<uint32_t>(mInFlightFences.size()), mInFlightFences.data(), VK_TRUE, UINT64_MAX
|
||||
);
|
||||
destroySwapchainResources();
|
||||
|
||||
VkSwapchainKHR previous = mSwapchain;
|
||||
mSwapchain = VK_NULL_HANDLE;
|
||||
createSwapchain(std::max(width, 1), std::max(height, 1), previous);
|
||||
mRequestedExtent = { (uint32_t) width, (uint32_t) height };
|
||||
mSwapchainRecreations++;
|
||||
}
|
||||
|
||||
bool VulkanContext::swapchainMatches(int framebufferWidth, int framebufferHeight) const {
|
||||
return mRequestedExtent.width == static_cast<uint32_t>(framebufferWidth) &&
|
||||
mRequestedExtent.height == static_cast<uint32_t>(framebufferHeight);
|
||||
}
|
||||
|
||||
bool VulkanContext::beginFrame(int framebufferWidth, int framebufferHeight) {
|
||||
if (framebufferWidth <= 0 || framebufferHeight <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (mSurface == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!swapchainMatches(framebufferWidth, framebufferHeight)) {
|
||||
recreateSwapchain(framebufferWidth, framebufferHeight);
|
||||
}
|
||||
|
||||
uint32_t imageIndex = 0;
|
||||
VkResult acquire = VK_SUCCESS;
|
||||
{
|
||||
TP_PROFILE_SCOPE("frame.acquire");
|
||||
vkWaitForFences(mDevice, 1, &mInFlightFences[mCurrentFrame], VK_TRUE, UINT64_MAX);
|
||||
acquire = vkAcquireNextImageKHR(
|
||||
mDevice, mSwapchain, UINT64_MAX, mImageAvailableSemaphores[mCurrentFrame], VK_NULL_HANDLE, &imageIndex
|
||||
);
|
||||
}
|
||||
|
||||
if (acquire == VK_ERROR_OUT_OF_DATE_KHR) {
|
||||
recreateSwapchain(framebufferWidth, framebufferHeight);
|
||||
return false;
|
||||
}
|
||||
if (acquire != VK_SUCCESS && acquire != VK_SUBOPTIMAL_KHR) {
|
||||
throw std::runtime_error("failed to acquire swapchain image");
|
||||
}
|
||||
|
||||
vkResetFences(mDevice, 1, &mInFlightFences[mCurrentFrame]);
|
||||
mAcquiredImageIndex = imageIndex;
|
||||
|
||||
VkCommandBuffer commandBuffer = mCommandBuffers[mCurrentFrame];
|
||||
vkResetCommandBuffer(commandBuffer, 0);
|
||||
|
||||
VkCommandBufferBeginInfo beginInfo{ .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
|
||||
vkBeginCommandBuffer(commandBuffer, &beginInfo);
|
||||
mFrameGpuTimer->begin(commandBuffer);
|
||||
|
||||
std::array<VkClearValue, 2> clearValues{};
|
||||
clearValues[0].color = { { 0.0f, 0.0f, 0.0f, isTranslucent() ? 0.0f : 1.0f } };
|
||||
clearValues[1].depthStencil = { 1.0f, 0 };
|
||||
|
||||
VkRenderPassBeginInfo renderPassInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
|
||||
.renderPass = mRenderPass,
|
||||
.framebuffer = mSwapchainFramebuffers[imageIndex],
|
||||
.renderArea = { .offset = { 0, 0 }, .extent = mSwapchainExtent },
|
||||
.clearValueCount = static_cast<uint32_t>(clearValues.size()),
|
||||
.pClearValues = clearValues.data(),
|
||||
};
|
||||
vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
|
||||
|
||||
VkViewport viewport{
|
||||
.x = 0.0f,
|
||||
.y = 0.0f,
|
||||
.width = static_cast<float>(mSwapchainExtent.width),
|
||||
.height = static_cast<float>(mSwapchainExtent.height),
|
||||
.minDepth = 0.0f,
|
||||
.maxDepth = 1.0f,
|
||||
};
|
||||
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
|
||||
|
||||
VkRect2D scissor{ { 0, 0 }, mSwapchainExtent };
|
||||
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
|
||||
|
||||
mFrameActive = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanContext::endFrame() {
|
||||
if (!mFrameActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
TP_PROFILE_SCOPE("frame.submit");
|
||||
VkCommandBuffer commandBuffer = mCommandBuffers[mCurrentFrame];
|
||||
vkCmdEndRenderPass(commandBuffer);
|
||||
mFrameGpuTimer->end(commandBuffer);
|
||||
vkEndCommandBuffer(commandBuffer);
|
||||
|
||||
VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||
VkSubmitInfo submitInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
|
||||
.waitSemaphoreCount = 1,
|
||||
.pWaitSemaphores = &mImageAvailableSemaphores[mCurrentFrame],
|
||||
.pWaitDstStageMask = &waitStage,
|
||||
.commandBufferCount = 1,
|
||||
.pCommandBuffers = &commandBuffer,
|
||||
.signalSemaphoreCount = 1,
|
||||
.pSignalSemaphores = &mRenderFinishedSemaphores[mCurrentFrame],
|
||||
};
|
||||
vkQueueSubmit(mGraphicsQueue, 1, &submitInfo, mInFlightFences[mCurrentFrame]);
|
||||
|
||||
VkPresentInfoKHR presentInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
|
||||
.waitSemaphoreCount = 1,
|
||||
.pWaitSemaphores = &mRenderFinishedSemaphores[mCurrentFrame],
|
||||
.swapchainCount = 1,
|
||||
.pSwapchains = &mSwapchain,
|
||||
.pImageIndices = &mAcquiredImageIndex,
|
||||
};
|
||||
VkResult present = vkQueuePresentKHR(mPresentQueue, &presentInfo);
|
||||
|
||||
if (present == VK_ERROR_OUT_OF_DATE_KHR || (present == VK_SUBOPTIMAL_KHR && surfaceChanged())) {
|
||||
const VkExtent2D extent = mSurfaceSource.framebufferExtent();
|
||||
recreateSwapchain((int) extent.width, (int) extent.height);
|
||||
}
|
||||
|
||||
mCurrentFrame = (mCurrentFrame + 1) % static_cast<uint32_t>(mCommandBuffers.size());
|
||||
mFrameActive = false;
|
||||
}
|
||||
|
||||
bool VulkanContext::surfaceChanged() const {
|
||||
VkSurfaceCapabilitiesKHR capabilities{};
|
||||
if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(mPhysicalDevice, mSurface, &capabilities) != VK_SUCCESS) {
|
||||
return true;
|
||||
}
|
||||
return capabilities.currentTransform != mSurfaceTransform ||
|
||||
capabilities.currentExtent.width != mSurfaceExtent.width ||
|
||||
capabilities.currentExtent.height != mSurfaceExtent.height;
|
||||
}
|
||||
|
||||
void VulkanContext::waitIdle() {
|
||||
if (mDevice != VK_NULL_HANDLE) {
|
||||
vkDeviceWaitIdle(mDevice);
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanContext::releaseSurface() {
|
||||
if (mSurface == VK_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
vkDeviceWaitIdle(mDevice);
|
||||
destroySwapchain();
|
||||
mSwapchainExtent = { 0, 0 };
|
||||
vkDestroySurfaceKHR(mInstance, mSurface, nullptr);
|
||||
mSurface = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
void VulkanContext::recreateSurface() {
|
||||
if (mSurface != VK_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
createSurface();
|
||||
VkBool32 presentSupport = VK_FALSE;
|
||||
vkGetPhysicalDeviceSurfaceSupportKHR(mPhysicalDevice, mPresentQueueFamily, mSurface, &presentSupport);
|
||||
if (!presentSupport) {
|
||||
throw std::runtime_error("recreated surface cannot be presented");
|
||||
}
|
||||
const VkExtent2D extent = mSurfaceSource.framebufferExtent();
|
||||
createSwapchain(std::max<int>((int) extent.width, 1), std::max<int>((int) extent.height, 1));
|
||||
}
|
||||
|
||||
void VulkanContext::syncSwapchainToWindow() {
|
||||
const VkExtent2D extent = mSurfaceSource.framebufferExtent();
|
||||
if (extent.width == 0 || extent.height == 0) {
|
||||
return;
|
||||
}
|
||||
recreateSwapchain((int) extent.width, (int) extent.height);
|
||||
}
|
||||
|
||||
VkCommandBuffer VulkanContext::beginOneShotCommands() 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;
|
||||
vkAllocateCommandBuffers(mDevice, &allocateInfo, &commandBuffer);
|
||||
|
||||
VkCommandBufferBeginInfo beginInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
|
||||
};
|
||||
vkBeginCommandBuffer(commandBuffer, &beginInfo);
|
||||
return commandBuffer;
|
||||
}
|
||||
|
||||
void VulkanContext::endOneShotCommands(VkCommandBuffer commandBuffer) const {
|
||||
vkEndCommandBuffer(commandBuffer);
|
||||
|
||||
// Use a dedicated fence and wait only on this submission, instead of
|
||||
// vkQueueWaitIdle which serializes the entire graphics queue (including
|
||||
// in-flight swapchain frames) and can stall the first window present.
|
||||
VkFenceCreateInfo fenceInfo{ .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
vkCreateFence(mDevice, &fenceInfo, nullptr, &fence);
|
||||
|
||||
VkSubmitInfo submitInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
|
||||
.commandBufferCount = 1,
|
||||
.pCommandBuffers = &commandBuffer,
|
||||
};
|
||||
vkQueueSubmit(mGraphicsQueue, 1, &submitInfo, fence);
|
||||
vkWaitForFences(mDevice, 1, &fence, VK_TRUE, UINT64_MAX);
|
||||
|
||||
vkDestroyFence(mDevice, fence, nullptr);
|
||||
vkFreeCommandBuffers(mDevice, mCommandPool, 1, &commandBuffer);
|
||||
}
|
||||
}
|
||||
42
GraphicApplication/src/render/public/GpuTimer.hpp
Normal file
42
GraphicApplication/src/render/public/GpuTimer.hpp
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
#include "Profiler.hpp"
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
class GpuTimer {
|
||||
public:
|
||||
static constexpr uint32_t kSlots = 8;
|
||||
|
||||
GpuTimer(VkDevice device, VkPhysicalDevice physicalDevice, uint32_t queueFamily, const char* name);
|
||||
~GpuTimer();
|
||||
|
||||
GpuTimer(const GpuTimer&) = delete;
|
||||
GpuTimer& operator=(const GpuTimer&) = delete;
|
||||
|
||||
[[nodiscard]] bool isSupported() const { return mPool != VK_NULL_HANDLE; }
|
||||
|
||||
void begin(VkCommandBuffer commandBuffer);
|
||||
void end(VkCommandBuffer commandBuffer);
|
||||
void collect();
|
||||
|
||||
[[nodiscard]] double lastMs() const { return mLastMs; }
|
||||
|
||||
private:
|
||||
VkDevice mDevice;
|
||||
VkQueryPool mPool = VK_NULL_HANDLE;
|
||||
double mTickNs = 1;
|
||||
uint64_t mValidMask = ~0ull;
|
||||
ProfileId mSection;
|
||||
|
||||
std::vector<bool> mPending;
|
||||
uint32_t mNextSlot = 0;
|
||||
uint32_t mOpenSlot = UINT32_MAX;
|
||||
double mLastMs = 0;
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
138
GraphicApplication/src/render/public/VulkanContext.hpp
Normal file
138
GraphicApplication/src/render/public/VulkanContext.hpp
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
#pragma once
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include "RenderTargetRegistry.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace tp {
|
||||
|
||||
class GpuTimer;
|
||||
|
||||
class VulkanSurfaceSource {
|
||||
public:
|
||||
virtual ~VulkanSurfaceSource() = default;
|
||||
|
||||
[[nodiscard]] virtual std::vector<const char*> requiredInstanceExtensions() const = 0;
|
||||
[[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;
|
||||
};
|
||||
|
||||
class VulkanContext {
|
||||
public:
|
||||
explicit VulkanContext(VulkanSurfaceSource& surfaceSource);
|
||||
~VulkanContext();
|
||||
|
||||
VulkanContext(const VulkanContext&) = delete;
|
||||
VulkanContext& operator=(const VulkanContext&) = delete;
|
||||
|
||||
VkInstance instance() const { return mInstance; }
|
||||
VkDevice device() const { return mDevice; }
|
||||
VkPhysicalDevice physicalDevice() const { return mPhysicalDevice; }
|
||||
VkQueue graphicsQueue() const { return mGraphicsQueue; }
|
||||
VkRenderPass renderPass() const { return mRenderPass; }
|
||||
uint32_t swapchainImageCount() const { return static_cast<uint32_t>(mSwapchainImages.size()); }
|
||||
VkExtent2D swapchainExtent() const { return mSwapchainExtent; }
|
||||
|
||||
VkCommandBuffer* commandBufferArray() { return mCommandBuffers.data(); }
|
||||
VkCommandBuffer activeCommandBuffer() const { return mCommandBuffers[mCurrentFrame]; }
|
||||
uint32_t* currentFrameSlot() { return &mCurrentFrame; }
|
||||
uint32_t framesInFlight() const { return kFramesInFlight; }
|
||||
bool isTranslucent() const { return mTranslucent; }
|
||||
|
||||
bool beginFrame(int framebufferWidth, int framebufferHeight);
|
||||
[[nodiscard]] uint64_t swapchainRecreations() const { return mSwapchainRecreations; }
|
||||
void endFrame();
|
||||
void waitIdle();
|
||||
|
||||
uint32_t graphicsQueueFamily() const { return mGraphicsQueueFamily; }
|
||||
VkCommandPool commandPool() const { return mCommandPool; }
|
||||
uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) const;
|
||||
|
||||
VkCommandBuffer beginOneShotCommands() const;
|
||||
void endOneShotCommands(VkCommandBuffer commandBuffer) const;
|
||||
|
||||
void syncSwapchainToWindow();
|
||||
void releaseSurface();
|
||||
void recreateSurface();
|
||||
[[nodiscard]] bool hasSurface() const { return mSurface != VK_NULL_HANDLE; }
|
||||
|
||||
[[nodiscard]] RenderTargetRegistry& renderTargets() { return mRenderTargets; }
|
||||
|
||||
private:
|
||||
uint64_t mSwapchainRecreations = 0;
|
||||
VkSurfaceTransformFlagBitsKHR mSurfaceTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
VkExtent2D mSurfaceExtent{ 0, 0 };
|
||||
VkExtent2D mRequestedExtent{ 0, 0 };
|
||||
void createInstance();
|
||||
void createSurface();
|
||||
void selectCompositeAlpha();
|
||||
void selectPhysicalDevice();
|
||||
void createLogicalDevice();
|
||||
void createCommandResources();
|
||||
void createSyncObjects();
|
||||
void createRenderPass();
|
||||
|
||||
bool swapchainMatches(int framebufferWidth, int framebufferHeight) const;
|
||||
VkPresentModeKHR selectPresentMode() const;
|
||||
void createSwapchain(int width, int height, VkSwapchainKHR previous = VK_NULL_HANDLE);
|
||||
void destroySwapchainResources();
|
||||
void destroySwapchain();
|
||||
void recreateSwapchain(int width, int height);
|
||||
[[nodiscard]] bool surfaceChanged() const;
|
||||
|
||||
VkFormat selectDepthStencilFormat() const;
|
||||
void createDepthStencil();
|
||||
void destroyDepthStencil();
|
||||
|
||||
VulkanSurfaceSource& mSurfaceSource;
|
||||
VkCompositeAlphaFlagBitsKHR mCompositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
|
||||
bool mTranslucent = false;
|
||||
|
||||
VkInstance mInstance = VK_NULL_HANDLE;
|
||||
VkSurfaceKHR mSurface = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice mPhysicalDevice = VK_NULL_HANDLE;
|
||||
VkDevice mDevice = VK_NULL_HANDLE;
|
||||
|
||||
uint32_t mGraphicsQueueFamily = UINT32_MAX;
|
||||
uint32_t mPresentQueueFamily = UINT32_MAX;
|
||||
VkQueue mGraphicsQueue = VK_NULL_HANDLE;
|
||||
VkQueue mPresentQueue = VK_NULL_HANDLE;
|
||||
|
||||
VkSwapchainKHR mSwapchain = VK_NULL_HANDLE;
|
||||
VkPresentModeKHR mPresentMode = VK_PRESENT_MODE_FIFO_KHR;
|
||||
VkFormat mSwapchainFormat{};
|
||||
VkExtent2D mSwapchainExtent{};
|
||||
std::vector<VkImage> mSwapchainImages;
|
||||
std::vector<VkImageView> mSwapchainImageViews;
|
||||
std::vector<VkFramebuffer> mSwapchainFramebuffers;
|
||||
|
||||
VkRenderPass mRenderPass = VK_NULL_HANDLE;
|
||||
|
||||
VkFormat mDepthStencilFormat{};
|
||||
VkImage mDepthStencilImage = VK_NULL_HANDLE;
|
||||
VkDeviceMemory mDepthStencilMemory = VK_NULL_HANDLE;
|
||||
VkImageView mDepthStencilView = VK_NULL_HANDLE;
|
||||
|
||||
static constexpr uint32_t kFramesInFlight = 2;
|
||||
|
||||
VkCommandPool mCommandPool = VK_NULL_HANDLE;
|
||||
std::vector<VkCommandBuffer> mCommandBuffers;
|
||||
|
||||
std::vector<VkSemaphore> mImageAvailableSemaphores;
|
||||
std::vector<VkSemaphore> mRenderFinishedSemaphores;
|
||||
std::vector<VkFence> mInFlightFences;
|
||||
|
||||
uint32_t mCurrentFrame = 0;
|
||||
uint32_t mAcquiredImageIndex = 0;
|
||||
bool mFrameActive = false;
|
||||
|
||||
std::unique_ptr<GpuTimer> mFrameGpuTimer;
|
||||
RenderTargetRegistry mRenderTargets;
|
||||
};
|
||||
}
|
||||
48
GraphicApplication/src/window/private/Window.cpp
Normal file
48
GraphicApplication/src/window/private/Window.cpp
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#include "Window.hpp"
|
||||
#include "WindowContext.hpp"
|
||||
|
||||
#include "WindowControls.hpp"
|
||||
#include "WindowEventLoop.hpp"
|
||||
#include "WindowInput.hpp"
|
||||
#include "WindowServices.hpp"
|
||||
|
||||
using namespace tp;
|
||||
|
||||
Window::Window(Vec2F size, const WindowIdentity& identity, void* backendContext) :
|
||||
mContext(std::make_unique<Context>(*this)) {
|
||||
mContext->platform = createPlatformWindow(mContext->host, size, identity, backendContext);
|
||||
mContext->vulkan = std::make_unique<VulkanContext>(*mContext->platform);
|
||||
mEventLoop = std::unique_ptr<WindowEventLoop>(new WindowEventLoop(*mContext->platform, *mContext->vulkan));
|
||||
mInput = std::unique_ptr<WindowInput>(new WindowInput(*mContext->platform, mInputState));
|
||||
mControls = std::unique_ptr<WindowControls>(new WindowControls(*mContext->platform));
|
||||
mServices = std::unique_ptr<WindowServices>(new WindowServices(*mContext->platform));
|
||||
}
|
||||
|
||||
Window::~Window() = default;
|
||||
|
||||
WindowEventLoop& Window::loop() { return *mEventLoop; }
|
||||
|
||||
WindowInput& Window::input() { return *mInput; }
|
||||
|
||||
WindowControls& Window::controls() { return *mControls; }
|
||||
|
||||
WindowServices& Window::services() { return *mServices; }
|
||||
|
||||
InputEventSink& Window::Host::inputSink() { return mWindow.mInputState; }
|
||||
|
||||
VulkanContext* Window::vulkan() { return mContext->vulkan.get(); }
|
||||
|
||||
Vec2F Window::getSize() const { return mContext->platform->contentSize(); }
|
||||
|
||||
Vec2F Window::getFramebufferSize() const {
|
||||
const VkExtent2D extent = mContext->platform->framebufferExtent();
|
||||
return { (halnf) extent.width, (halnf) extent.height };
|
||||
}
|
||||
|
||||
halnf Window::getContentScale() const { return mContext->platform->scale(); }
|
||||
|
||||
const WindowFrame& Window::frame() const { return mContext->platform->frame(); }
|
||||
|
||||
PlatformInfo Window::platformInfo() const { return mContext->platform->platformInfo(); }
|
||||
|
||||
void Window::onContentResized() { mInputState.onResized(); }
|
||||
34
GraphicApplication/src/window/private/WindowContext.hpp
Normal file
34
GraphicApplication/src/window/private/WindowContext.hpp
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#pragma once
|
||||
|
||||
#include "Window.hpp"
|
||||
|
||||
#include "VulkanContext.hpp"
|
||||
#include "PlatformWindow.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace tp {
|
||||
|
||||
class Window::Host final : public PlatformWindowHost {
|
||||
public:
|
||||
explicit Host(Window& window) :
|
||||
mWindow(window) {}
|
||||
|
||||
[[nodiscard]] InputEventSink& inputSink() override;
|
||||
void onContentResized() override { mWindow.onContentResized(); }
|
||||
[[nodiscard]] VulkanContext* vulkan() override { return mWindow.vulkan(); }
|
||||
|
||||
private:
|
||||
Window& mWindow;
|
||||
};
|
||||
|
||||
class Window::Context {
|
||||
public:
|
||||
explicit Context(Window& window) :
|
||||
host(window) {}
|
||||
|
||||
Host host;
|
||||
std::unique_ptr<PlatformWindow> platform;
|
||||
std::unique_ptr<VulkanContext> vulkan;
|
||||
};
|
||||
}
|
||||
51
GraphicApplication/src/window/private/WindowControls.cpp
Normal file
51
GraphicApplication/src/window/private/WindowControls.cpp
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#include "WindowControls.hpp"
|
||||
|
||||
#include "PlatformWindow.hpp"
|
||||
#include "PlatformWindowDesktopControls.hpp"
|
||||
|
||||
using namespace tp;
|
||||
|
||||
WindowControls::WindowControls(PlatformWindow& platform) :
|
||||
mPlatform(platform) {}
|
||||
|
||||
bool WindowControls::shouldClose() const { return mPlatform.shouldClose(); }
|
||||
|
||||
void WindowControls::requestClose() { mPlatform.requestClose(); }
|
||||
|
||||
void WindowControls::minimize() { mPlatform.minimize(); }
|
||||
|
||||
void WindowControls::toggleMaximize() {
|
||||
if (PlatformWindowDesktopControls* controls = mPlatform.desktopControls()) {
|
||||
controls->toggleMaximize();
|
||||
}
|
||||
}
|
||||
|
||||
bool WindowControls::isMaximized() const { return mPlatform.isMaximized(); }
|
||||
|
||||
void WindowControls::setFullScreen(bool fullScreen) {
|
||||
if (PlatformWindowDesktopControls* controls = mPlatform.desktopControls()) {
|
||||
controls->setFullScreen(fullScreen);
|
||||
}
|
||||
}
|
||||
|
||||
bool WindowControls::isFullScreen() const { return mPlatform.isFullScreen(); }
|
||||
|
||||
bool WindowControls::isFocused() const { return mPlatform.isFocused(); }
|
||||
|
||||
void WindowControls::beginMove() {
|
||||
if (PlatformWindowDesktopControls* controls = mPlatform.desktopControls()) {
|
||||
controls->beginMove();
|
||||
}
|
||||
}
|
||||
|
||||
void WindowControls::beginResize(WindowEdge edge) {
|
||||
if (PlatformWindowDesktopControls* controls = mPlatform.desktopControls()) {
|
||||
controls->beginResize(edge);
|
||||
}
|
||||
}
|
||||
|
||||
void WindowControls::showWindowMenu(Vec2F contentPosition) {
|
||||
if (PlatformWindowDesktopControls* controls = mPlatform.desktopControls()) {
|
||||
controls->showWindowMenu(contentPosition);
|
||||
}
|
||||
}
|
||||
42
GraphicApplication/src/window/private/WindowEventLoop.cpp
Normal file
42
GraphicApplication/src/window/private/WindowEventLoop.cpp
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#include "WindowEventLoop.hpp"
|
||||
|
||||
#include "PlatformWindow.hpp"
|
||||
#include "VulkanContext.hpp"
|
||||
|
||||
using namespace tp;
|
||||
|
||||
WindowEventLoop::WindowEventLoop(PlatformWindow& platform, VulkanContext& vulkan) :
|
||||
mPlatform(platform),
|
||||
mVulkan(vulkan) {}
|
||||
|
||||
void WindowEventLoop::processEvents(bool wait) { mPlatform.dispatchEvents(wait ? -1.0f : 0.0f); }
|
||||
|
||||
void WindowEventLoop::processEventsFor(halnf timeoutMs) { mPlatform.dispatchEvents(timeoutMs > 0 ? timeoutMs : 0.0f); }
|
||||
|
||||
void WindowEventLoop::wake() { mPlatform.wake(); }
|
||||
|
||||
bool WindowEventLoop::isWaitingForFrame() const { return mPlatform.isWaitingForFrame(); }
|
||||
|
||||
bool WindowEventLoop::hasPendingWork() const {
|
||||
const InputCoalescer& coalescer = mPlatform.coalescer();
|
||||
return !coalescer.isEmpty() || coalescer.hasPendingConfigure();
|
||||
}
|
||||
|
||||
halnf WindowEventLoop::refreshIntervalMs() const { return mPlatform.refreshIntervalMs(); }
|
||||
|
||||
uint64_t WindowEventLoop::swapchainRecreations() const { return mVulkan.swapchainRecreations(); }
|
||||
|
||||
void WindowEventLoop::waitForReady() {
|
||||
mPlatform.waitUntilConfigured();
|
||||
mVulkan.syncSwapchainToWindow();
|
||||
}
|
||||
|
||||
bool WindowEventLoop::frameBegin() {
|
||||
const VkExtent2D extent = mPlatform.framebufferExtent();
|
||||
return mVulkan.beginFrame((int) extent.width, (int) extent.height);
|
||||
}
|
||||
|
||||
void WindowEventLoop::frameEnd() {
|
||||
mPlatform.commitFrameGeometry();
|
||||
mVulkan.endFrame();
|
||||
}
|
||||
29
GraphicApplication/src/window/private/WindowInput.cpp
Normal file
29
GraphicApplication/src/window/private/WindowInput.cpp
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#include "WindowInput.hpp"
|
||||
|
||||
#include "PlatformWindow.hpp"
|
||||
|
||||
using namespace tp;
|
||||
|
||||
WindowInput::WindowInput(PlatformWindow& platform, InputState& inputState) :
|
||||
mPlatform(platform),
|
||||
mInputState(inputState) {}
|
||||
|
||||
void WindowInput::flushPendingInput() { mPlatform.flushPendingInput(); }
|
||||
|
||||
InputEventSink& WindowInput::injectInput() { return mPlatform.coalescer().recorder(); }
|
||||
|
||||
void WindowInput::setRawInputTap(InputEventSink* tap) { mPlatform.coalescer().setTap(tap); }
|
||||
|
||||
void WindowInput::setDeliveryPolicies(const InputDeliveryPolicies& policies) {
|
||||
mPlatform.coalescer().setPolicies(policies);
|
||||
}
|
||||
|
||||
const CoalescingStatistics& WindowInput::statistics() const { return mPlatform.coalescer().statistics(); }
|
||||
|
||||
void WindowInput::setPlatformInputEnabled(bool enabled) {
|
||||
if (enabled == mPlatformInputEnabled) {
|
||||
return;
|
||||
}
|
||||
mPlatformInputEnabled = enabled;
|
||||
mPlatform.setAcceptsInput(enabled);
|
||||
}
|
||||
22
GraphicApplication/src/window/private/WindowServices.cpp
Normal file
22
GraphicApplication/src/window/private/WindowServices.cpp
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#include "WindowServices.hpp"
|
||||
|
||||
#include "PlatformWindow.hpp"
|
||||
|
||||
using namespace tp;
|
||||
|
||||
WindowServices::WindowServices(PlatformWindow& platform) :
|
||||
mPlatform(platform) {}
|
||||
|
||||
void WindowServices::setCursor(CursorShape shape) { mPlatform.setCursor(shape); }
|
||||
|
||||
void WindowServices::setClipboardText(const std::string& text) { mPlatform.setClipboardText(text); }
|
||||
|
||||
std::string WindowServices::clipboardText() { return mPlatform.clipboardText(); }
|
||||
|
||||
ClipboardImage WindowServices::clipboardImage() { return mPlatform.clipboardImage(); }
|
||||
|
||||
bool WindowServices::openUrl(const std::string& url) { return mPlatform.openUrl(url); }
|
||||
|
||||
void WindowServices::setTextInputEnabled(bool enabled) { mPlatform.setTextInputEnabled(enabled); }
|
||||
|
||||
void WindowServices::setTextInputRect(const RectF& contentRect) { mPlatform.setTextInputRect(contentRect); }
|
||||
55
GraphicApplication/src/window/public/Window.hpp
Normal file
55
GraphicApplication/src/window/public/Window.hpp
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputState.hpp"
|
||||
#include "PlatformInfo.hpp"
|
||||
#include "WindowControls.hpp"
|
||||
#include "WindowEventLoop.hpp"
|
||||
#include "WindowFrame.hpp"
|
||||
#include "WindowInput.hpp"
|
||||
#include "WindowServices.hpp"
|
||||
#include "WindowTypes.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace tp {
|
||||
class VulkanContext;
|
||||
class WindowEventLoop;
|
||||
class WindowInput;
|
||||
class WindowControls;
|
||||
class WindowServices;
|
||||
|
||||
class Window {
|
||||
class Context;
|
||||
|
||||
public:
|
||||
Window(Vec2F size, const WindowIdentity& identity, void* backendContext = nullptr);
|
||||
~Window();
|
||||
|
||||
Window(const Window&) = delete;
|
||||
Window& operator=(const Window&) = delete;
|
||||
|
||||
[[nodiscard]] WindowEventLoop& loop();
|
||||
[[nodiscard]] WindowInput& input();
|
||||
[[nodiscard]] WindowControls& controls();
|
||||
[[nodiscard]] WindowServices& services();
|
||||
|
||||
[[nodiscard]] VulkanContext* vulkan();
|
||||
[[nodiscard]] Vec2F getSize() const;
|
||||
[[nodiscard]] Vec2F getFramebufferSize() const;
|
||||
[[nodiscard]] halnf getContentScale() const;
|
||||
[[nodiscard]] const WindowFrame& frame() const;
|
||||
[[nodiscard]] PlatformInfo platformInfo() const;
|
||||
|
||||
private:
|
||||
class Host;
|
||||
|
||||
void onContentResized();
|
||||
|
||||
std::unique_ptr<Context> mContext;
|
||||
InputState mInputState;
|
||||
std::unique_ptr<WindowEventLoop> mEventLoop;
|
||||
std::unique_ptr<WindowInput> mInput;
|
||||
std::unique_ptr<WindowControls> mControls;
|
||||
std::unique_ptr<WindowServices> mServices;
|
||||
};
|
||||
}
|
||||
29
GraphicApplication/src/window/public/WindowControls.hpp
Normal file
29
GraphicApplication/src/window/public/WindowControls.hpp
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include "Vec.hpp"
|
||||
#include "WindowTypes.hpp"
|
||||
|
||||
namespace tp {
|
||||
class PlatformWindow;
|
||||
|
||||
class WindowControls {
|
||||
public:
|
||||
[[nodiscard]] bool shouldClose() const;
|
||||
void requestClose();
|
||||
void minimize();
|
||||
void toggleMaximize();
|
||||
[[nodiscard]] bool isMaximized() const;
|
||||
void setFullScreen(bool fullScreen);
|
||||
[[nodiscard]] bool isFullScreen() const;
|
||||
[[nodiscard]] bool isFocused() const;
|
||||
void beginMove();
|
||||
void beginResize(WindowEdge edge);
|
||||
void showWindowMenu(Vec2F contentPosition);
|
||||
|
||||
private:
|
||||
friend class Window;
|
||||
explicit WindowControls(PlatformWindow& platform);
|
||||
|
||||
PlatformWindow& mPlatform;
|
||||
};
|
||||
}
|
||||
32
GraphicApplication/src/window/public/WindowEventLoop.hpp
Normal file
32
GraphicApplication/src/window/public/WindowEventLoop.hpp
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#include "Vec.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace tp {
|
||||
class PlatformWindow;
|
||||
class VulkanContext;
|
||||
|
||||
class WindowEventLoop {
|
||||
public:
|
||||
void processEvents(bool wait = true);
|
||||
void processEventsFor(halnf timeoutMs);
|
||||
void wake();
|
||||
[[nodiscard]] bool isWaitingForFrame() const;
|
||||
[[nodiscard]] bool hasPendingWork() const;
|
||||
[[nodiscard]] halnf refreshIntervalMs() const;
|
||||
[[nodiscard]] uint64_t swapchainRecreations() const;
|
||||
void waitForReady();
|
||||
|
||||
bool frameBegin();
|
||||
void frameEnd();
|
||||
|
||||
private:
|
||||
friend class Window;
|
||||
WindowEventLoop(PlatformWindow& platform, VulkanContext& vulkan);
|
||||
|
||||
PlatformWindow& mPlatform;
|
||||
VulkanContext& mVulkan;
|
||||
};
|
||||
}
|
||||
29
GraphicApplication/src/window/public/WindowInput.hpp
Normal file
29
GraphicApplication/src/window/public/WindowInput.hpp
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include "InputCoalescer.hpp"
|
||||
#include "InputEventSink.hpp"
|
||||
#include "InputState.hpp"
|
||||
|
||||
namespace tp {
|
||||
class PlatformWindow;
|
||||
|
||||
class WindowInput {
|
||||
public:
|
||||
[[nodiscard]] InputState& state() { return mInputState; }
|
||||
void flushPendingInput();
|
||||
[[nodiscard]] InputEventSink& injectInput();
|
||||
void setRawInputTap(InputEventSink* tap);
|
||||
void setDeliveryPolicies(const InputDeliveryPolicies& policies);
|
||||
[[nodiscard]] const CoalescingStatistics& statistics() const;
|
||||
void setPlatformInputEnabled(bool enabled);
|
||||
[[nodiscard]] bool isPlatformInputEnabled() const { return mPlatformInputEnabled; }
|
||||
|
||||
private:
|
||||
friend class Window;
|
||||
WindowInput(PlatformWindow& platform, InputState& inputState);
|
||||
|
||||
PlatformWindow& mPlatform;
|
||||
InputState& mInputState;
|
||||
bool mPlatformInputEnabled = true;
|
||||
};
|
||||
}
|
||||
27
GraphicApplication/src/window/public/WindowServices.hpp
Normal file
27
GraphicApplication/src/window/public/WindowServices.hpp
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
|
||||
#include "Rect.hpp"
|
||||
#include "WindowTypes.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace tp {
|
||||
class PlatformWindow;
|
||||
|
||||
class WindowServices {
|
||||
public:
|
||||
void setCursor(CursorShape shape);
|
||||
void setClipboardText(const std::string& text);
|
||||
[[nodiscard]] std::string clipboardText();
|
||||
[[nodiscard]] ClipboardImage clipboardImage();
|
||||
bool openUrl(const std::string& url);
|
||||
void setTextInputEnabled(bool enabled);
|
||||
void setTextInputRect(const RectF& contentRect);
|
||||
|
||||
private:
|
||||
friend class Window;
|
||||
explicit WindowServices(PlatformWindow& platform);
|
||||
|
||||
PlatformWindow& mPlatform;
|
||||
};
|
||||
}
|
||||
196
GraphicApplication/tests/FontCoverageTests.cpp
Normal file
196
GraphicApplication/tests/FontCoverageTests.cpp
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
#include "CanvasFonts.hpp"
|
||||
|
||||
#include "stb_truetype.h"
|
||||
|
||||
#include "UnitTest++/UnitTest++.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <regex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
const std::filesystem::path kSourceRoot = MODULES_SOURCE_DIR;
|
||||
const std::filesystem::path kResourceRoot = kSourceRoot / "GraphicApplication" / "rsc";
|
||||
const std::vector<std::string> kUiSourceDirectories = {
|
||||
"Widgets/private", "Widgets/public", "Taskboard/private", "Taskboard/public"
|
||||
};
|
||||
|
||||
class LoadedFont {
|
||||
public:
|
||||
explicit LoadedFont(const tp::FontFile& file) {
|
||||
std::ifstream stream(kResourceRoot / file.path, std::ios::binary);
|
||||
mBytes.assign(std::istreambuf_iterator<char>(stream), {});
|
||||
const auto* data = reinterpret_cast<const unsigned char*>(mBytes.data());
|
||||
mValid = !mBytes.empty() && stbtt_InitFont(&mInfo, data, stbtt_GetFontOffsetForIndex(data, 0));
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isValid() const { return mValid; }
|
||||
[[nodiscard]] bool hasGlyph(char32_t codepoint) const {
|
||||
return mValid && stbtt_FindGlyphIndex(&mInfo, int(codepoint)) != 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<char> mBytes;
|
||||
stbtt_fontinfo mInfo{};
|
||||
bool mValid = false;
|
||||
};
|
||||
|
||||
std::optional<char32_t> decodeUtf8(const std::string& text, size_t& position) {
|
||||
const auto byte = [&](size_t offset) { return static_cast<unsigned char>(text[position + offset]); };
|
||||
const unsigned char lead = byte(0);
|
||||
const size_t length = lead < 0x80 ? 1 : lead >= 0xF0 ? 4 : lead >= 0xE0 ? 3 : lead >= 0xC0 ? 2 : 0;
|
||||
if (length == 0 || position + length > text.size()) {
|
||||
position++;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
char32_t codepoint = length == 1 ? lead : lead & (0xFF >> (length + 1));
|
||||
for (size_t index = 1; index < length; index++) {
|
||||
codepoint = (codepoint << 6) | (byte(index) & 0x3F);
|
||||
}
|
||||
position += length;
|
||||
return codepoint;
|
||||
}
|
||||
|
||||
void collectEscapes(const std::string& source, std::set<char32_t>& codepoints) {
|
||||
static const std::regex escape(R"(\\u([0-9A-Fa-f]{4})|\\U([0-9A-Fa-f]{8}))");
|
||||
for (std::sregex_iterator match(source.begin(), source.end(), escape), end; match != end; ++match) {
|
||||
const std::string digits = (*match)[1].matched ? (*match)[1].str() : (*match)[2].str();
|
||||
codepoints.insert(char32_t(std::stoul(digits, nullptr, 16)));
|
||||
}
|
||||
}
|
||||
|
||||
void collectLiterals(const std::string& source, std::set<char32_t>& codepoints) {
|
||||
for (size_t position = 0; position < source.size();) {
|
||||
const std::optional<char32_t> codepoint = decodeUtf8(source, position);
|
||||
if (codepoint && *codepoint >= 0x80) {
|
||||
codepoints.insert(*codepoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::set<char32_t> codepointsUsedByUi() {
|
||||
std::set<char32_t> codepoints;
|
||||
for (const std::string& directory : kUiSourceDirectories) {
|
||||
if (!std::filesystem::is_directory(kSourceRoot / directory)) {
|
||||
continue;
|
||||
}
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(kSourceRoot / directory)) {
|
||||
if (!entry.is_regular_file()) {
|
||||
continue;
|
||||
}
|
||||
std::ifstream stream(entry.path(), std::ios::binary);
|
||||
const std::string source{ std::istreambuf_iterator<char>(stream), {} };
|
||||
collectEscapes(source, codepoints);
|
||||
collectLiterals(source, codepoints);
|
||||
}
|
||||
}
|
||||
return codepoints;
|
||||
}
|
||||
|
||||
std::string hex(char32_t codepoint) {
|
||||
char buffer[16];
|
||||
std::snprintf(buffer, sizeof(buffer), "U+%04X", unsigned(codepoint));
|
||||
return buffer;
|
||||
}
|
||||
|
||||
struct FaceChain {
|
||||
const char* label;
|
||||
tp::FontFile face;
|
||||
std::vector<tp::FontFile> fallbacks;
|
||||
};
|
||||
|
||||
std::vector<FaceChain> faceChains() {
|
||||
return {
|
||||
{ "regular", tp::kRegularFont, { tp::kRegularFallbacks.begin(), tp::kRegularFallbacks.end() } },
|
||||
{ "bold", tp::kBoldFont, { tp::kBoldFallbacks.begin(), tp::kBoldFallbacks.end() } },
|
||||
{ "monospace", tp::kMonospaceFont, { tp::kMonospaceFallbacks.begin(), tp::kMonospaceFallbacks.end() } },
|
||||
};
|
||||
}
|
||||
|
||||
const char* const kRussianAlphabetAndPunctuation =
|
||||
"АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя№«»—–…";
|
||||
|
||||
std::set<char32_t> codepointsOf(const char* text) {
|
||||
std::set<char32_t> codepoints;
|
||||
const std::string source{ text };
|
||||
for (size_t position = 0; position < source.size();) {
|
||||
const std::optional<char32_t> codepoint = decodeUtf8(source, position);
|
||||
if (codepoint) {
|
||||
codepoints.insert(*codepoint);
|
||||
}
|
||||
}
|
||||
return codepoints;
|
||||
}
|
||||
|
||||
void checkChainCovers(const FaceChain& chain, const std::set<char32_t>& codepoints) {
|
||||
const LoadedFont primary(chain.face);
|
||||
std::vector<LoadedFont> fallbacks;
|
||||
for (const tp::FontFile& file : chain.fallbacks) {
|
||||
fallbacks.emplace_back(file);
|
||||
}
|
||||
|
||||
for (const char32_t codepoint : codepoints) {
|
||||
bool covered = primary.hasGlyph(codepoint);
|
||||
for (const LoadedFont& fallback : fallbacks) {
|
||||
covered = covered || fallback.hasGlyph(codepoint);
|
||||
}
|
||||
CHECK_EQUAL(
|
||||
std::string(chain.label) + " " + hex(codepoint) + " drawn",
|
||||
std::string(chain.label) + " " + hex(codepoint) + (covered ? " drawn" : " has no glyph")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Chain>
|
||||
bool chainContains(const Chain& chain, const tp::FontFile& font) {
|
||||
for (const tp::FontFile& file : chain) {
|
||||
if (std::strcmp(file.path, font.path) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
SUITE(FontCoverage) {
|
||||
TEST(EveryBundledFontLoads) {
|
||||
for (const tp::FontFile& file : tp::kTextFonts) {
|
||||
CHECK(LoadedFont(file).isValid());
|
||||
}
|
||||
for (const tp::FontFile& file : tp::kFallbackFonts) {
|
||||
CHECK(LoadedFont(file).isValid());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(EveryCharacterTheUiDrawsHasAGlyphInEveryTextFace) {
|
||||
const std::set<char32_t> used = codepointsUsedByUi();
|
||||
for (const FaceChain& chain : faceChains()) {
|
||||
checkChainCovers(chain, used);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RussianTextHasAGlyphInEveryTextFace) {
|
||||
const std::set<char32_t> russian = codepointsOf(kRussianAlphabetAndPunctuation);
|
||||
CHECK_EQUAL(size_t(72), russian.size());
|
||||
|
||||
for (const FaceChain& chain : faceChains()) {
|
||||
checkChainCovers(chain, russian);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(EachFaceFallsBackToTheCyrillicCutOfItsOwnWeight) {
|
||||
CHECK(chainContains(tp::kRegularFallbacks, tp::kCyrillicRegularFont));
|
||||
CHECK(chainContains(tp::kMonospaceFallbacks, tp::kCyrillicRegularFont));
|
||||
CHECK(chainContains(tp::kBoldFallbacks, tp::kCyrillicBoldFont));
|
||||
CHECK(!chainContains(tp::kBoldFallbacks, tp::kCyrillicRegularFont));
|
||||
CHECK(!chainContains(tp::kRegularFallbacks, tp::kCyrillicBoldFont));
|
||||
}
|
||||
}
|
||||
251
GraphicApplication/tests/HangWatchdogTests.cpp
Normal file
251
GraphicApplication/tests/HangWatchdogTests.cpp
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
#include "HangWatchdog.hpp"
|
||||
#include "UiTrail.hpp"
|
||||
|
||||
#include "UnitTest++/UnitTest++.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <condition_variable>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
using namespace tp;
|
||||
|
||||
namespace {
|
||||
struct TempDirectory {
|
||||
std::filesystem::path path;
|
||||
|
||||
explicit TempDirectory(const std::string& name) :
|
||||
path(std::filesystem::temp_directory_path() / ("hang-watchdog-test-" + name)) {
|
||||
std::filesystem::remove_all(path);
|
||||
}
|
||||
~TempDirectory() {
|
||||
if (!std::getenv("KEEP_HANG_REPORTS")) {
|
||||
std::filesystem::remove_all(path);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
HangSettings fastSettings(const std::filesystem::path& directory) {
|
||||
HangSettings settings;
|
||||
settings.thresholdMs = 150;
|
||||
settings.followUpMs = {};
|
||||
settings.stackSamples = 3;
|
||||
settings.stackSampleIntervalMs = 30;
|
||||
settings.application = "test";
|
||||
settings.build = "0";
|
||||
settings.directory = directory;
|
||||
return settings;
|
||||
}
|
||||
|
||||
std::string readFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path);
|
||||
std::stringstream text;
|
||||
text << file.rdbuf();
|
||||
return text.str();
|
||||
}
|
||||
|
||||
struct TestScope {
|
||||
explicit TestScope(const char* name) { UiTrail::push(name); }
|
||||
~TestScope() { UiTrail::pop(); }
|
||||
};
|
||||
|
||||
bool hasVerdict(const std::filesystem::path& report) {
|
||||
return readFile(report / "summary.txt").find("verdict:") != std::string::npos;
|
||||
}
|
||||
|
||||
template <typename Done>
|
||||
bool waitUntil(Done&& done, double limitMs = 5000) {
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
while (!done()) {
|
||||
if (std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count() > limitMs) {
|
||||
return false;
|
||||
}
|
||||
std::this_thread::yield();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
class Gate {
|
||||
public:
|
||||
void waitOpen() {
|
||||
std::unique_lock lock(mMutex);
|
||||
mOpened.wait(lock, [this]() { return mOpen; });
|
||||
}
|
||||
void open() {
|
||||
{
|
||||
std::lock_guard lock(mMutex);
|
||||
mOpen = true;
|
||||
}
|
||||
mOpened.notify_all();
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mMutex;
|
||||
std::condition_variable mOpened;
|
||||
bool mOpen = false;
|
||||
};
|
||||
}
|
||||
|
||||
SUITE(HangWatchdog) {
|
||||
TEST(StuckDrawWritesAReportNamingTheOpenScopes) {
|
||||
TempDirectory directory("stuck");
|
||||
HangWatchdog watchdog(fastSettings(directory.path));
|
||||
Gate gate;
|
||||
watchdog.enterPhase(UiPhase::Draw);
|
||||
{
|
||||
TestScope outer("test.outer");
|
||||
TestScope waiting("test.waiting_on_gate");
|
||||
std::thread release([&]() {
|
||||
waitUntil([&]() { return watchdog.reportsWritten() > 0 && hasVerdict(watchdog.reports().front()); });
|
||||
gate.open();
|
||||
});
|
||||
gate.waitOpen();
|
||||
release.join();
|
||||
}
|
||||
watchdog.enterPhase(UiPhase::Idle);
|
||||
REQUIRE CHECK_EQUAL(1u, watchdog.reports().size());
|
||||
const std::string summary = readFile(watchdog.reports().front() / "summary.txt");
|
||||
CHECK(summary.find("drawing") != std::string::npos);
|
||||
CHECK(summary.find("test.outer") != std::string::npos);
|
||||
CHECK(summary.find("test.waiting_on_gate") != std::string::npos);
|
||||
CHECK(summary.find("verdict: blocked") != std::string::npos);
|
||||
const std::string threads = readFile(watchdog.reports().front() / "threads.txt");
|
||||
CHECK(threads.find("UI THREAD") != std::string::npos);
|
||||
CHECK(
|
||||
threads.find("waitOpen") != std::string::npos || threads.find("pthread_cond") != std::string::npos ||
|
||||
threads.find("libc") != std::string::npos
|
||||
);
|
||||
CHECK(waitUntil([&]() {
|
||||
return readFile(watchdog.reports().front() / "summary.txt").find("recovered") != std::string::npos;
|
||||
}));
|
||||
}
|
||||
|
||||
TEST(SpinningUiThreadIsReportedAsBusy) {
|
||||
TempDirectory directory("busy");
|
||||
HangWatchdog watchdog(fastSettings(directory.path));
|
||||
watchdog.enterPhase(UiPhase::Input);
|
||||
{
|
||||
TestScope spinning("test.spinning");
|
||||
volatile uint64_t work = 0;
|
||||
while (watchdog.reportsWritten() == 0 || !hasVerdict(watchdog.reports().front())) {
|
||||
work = work + 1;
|
||||
}
|
||||
}
|
||||
watchdog.enterPhase(UiPhase::Idle);
|
||||
const std::string summary = readFile(watchdog.reports().front() / "summary.txt");
|
||||
CHECK(summary.find("test.spinning") != std::string::npos);
|
||||
CHECK(summary.find("verdict: busy") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST(AProcessPauseIsNotCountedAsAHang) {
|
||||
TempDirectory directory("paused");
|
||||
std::atomic<int64_t> clock{ 0 };
|
||||
HangSettings settings = fastSettings(directory.path);
|
||||
settings.thresholdMs = 2000;
|
||||
settings.processPauseMs = 1000;
|
||||
settings.clockNs = [&clock]() { return clock.load(); };
|
||||
HangWatchdog watchdog(settings);
|
||||
watchdog.enterPhase(UiPhase::Draw);
|
||||
|
||||
const auto step = [&](int64_t milliseconds) {
|
||||
const uint64_t seen = watchdog.inspections();
|
||||
clock.fetch_add(milliseconds * 1000000);
|
||||
REQUIRE CHECK(waitUntil([&]() { return watchdog.inspections() > seen; }));
|
||||
};
|
||||
step(100);
|
||||
step(30000);
|
||||
step(100);
|
||||
CHECK_EQUAL(0u, watchdog.reportsWritten());
|
||||
|
||||
for (int tick = 0; tick < 25; tick++) {
|
||||
step(100);
|
||||
}
|
||||
CHECK_EQUAL(1u, watchdog.reportsWritten());
|
||||
watchdog.enterPhase(UiPhase::Idle);
|
||||
}
|
||||
|
||||
TEST(SummaryIsWrittenBeforeThreadsAreSampled) {
|
||||
TempDirectory directory("summary-first");
|
||||
HangSettings settings = fastSettings(directory.path);
|
||||
std::filesystem::path seenReport;
|
||||
bool summaryExisted = false;
|
||||
HangWatchdog watchdog(settings);
|
||||
watchdog.addReportFileSource([&]() {
|
||||
seenReport = watchdog.reports().empty() ? std::filesystem::path() : watchdog.reports().front();
|
||||
summaryExisted = !seenReport.empty() && std::filesystem::exists(seenReport / "summary.txt");
|
||||
return std::vector<HangReportFile>{};
|
||||
});
|
||||
watchdog.enterPhase(UiPhase::Present);
|
||||
REQUIRE CHECK(waitUntil([&]() { return watchdog.reportsWritten() > 0; }));
|
||||
watchdog.enterPhase(UiPhase::Idle);
|
||||
CHECK(summaryExisted);
|
||||
const std::string summary = readFile(seenReport / "summary.txt");
|
||||
CHECK(summary.find("debugger attached no") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST(InputEventsHandledInOnePhaseAreCounted) {
|
||||
TempDirectory directory("input-loop");
|
||||
HangWatchdog watchdog(fastSettings(directory.path));
|
||||
watchdog.enterPhase(UiPhase::Input);
|
||||
{
|
||||
TestScope inputLoop("test.input_loop");
|
||||
while (watchdog.reportsWritten() == 0 || !hasVerdict(watchdog.reports().front())) {
|
||||
watchdog.noteInputEvent();
|
||||
}
|
||||
}
|
||||
watchdog.enterPhase(UiPhase::Idle);
|
||||
const std::string summary = readFile(watchdog.reports().front() / "summary.txt");
|
||||
CHECK(summary.find("input events handled during this phase 0") == std::string::npos);
|
||||
CHECK(summary.find("input events were handled without leaving the input phase") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST(IdleNeverProducesAReport) {
|
||||
TempDirectory directory("idle");
|
||||
HangWatchdog watchdog(fastSettings(directory.path));
|
||||
watchdog.enterPhase(UiPhase::Idle);
|
||||
Gate gate;
|
||||
std::thread release([&]() {
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
waitUntil([&]() { return std::chrono::steady_clock::now() - start > std::chrono::milliseconds(600); });
|
||||
gate.open();
|
||||
});
|
||||
gate.waitOpen();
|
||||
release.join();
|
||||
CHECK_EQUAL(0u, watchdog.reportsWritten());
|
||||
}
|
||||
|
||||
TEST(ReportFileSourcesAddTheirOwnFiles) {
|
||||
TempDirectory directory("reportFileSources");
|
||||
HangWatchdog watchdog(fastSettings(directory.path));
|
||||
watchdog.addReportFileSource([]() { return std::vector<HangReportFile>{ { "engine.txt", "queue 3\n" } }; });
|
||||
watchdog.enterPhase(UiPhase::Input);
|
||||
REQUIRE CHECK(waitUntil([&]() { return watchdog.reportsWritten() > 0; }));
|
||||
watchdog.enterPhase(UiPhase::Idle);
|
||||
CHECK_EQUAL(std::string("queue 3\n"), readFile(watchdog.reports().front() / "engine.txt"));
|
||||
}
|
||||
|
||||
TEST(OnlyTheNewestReportsAreKept) {
|
||||
TempDirectory directory("retention");
|
||||
std::filesystem::create_directories(directory.path);
|
||||
for (int index = 0; index < 5; index++) {
|
||||
std::filesystem::create_directories(directory.path / ("old-" + std::to_string(index)));
|
||||
}
|
||||
HangSettings settings = fastSettings(directory.path);
|
||||
settings.keptReports = 2;
|
||||
HangWatchdog watchdog(settings);
|
||||
CHECK_EQUAL(2u, watchdog.reports().size());
|
||||
}
|
||||
|
||||
TEST(DirectoryIsTheApplicationsHangFolderUnlessOverridden) {
|
||||
const std::filesystem::path application = "/home/someone/sketch2d/hangs";
|
||||
CHECK(HangWatchdog::directoryFor(application) == application);
|
||||
setenv("MODULES_HANG_DIR", "/tmp/custom-hangs", 1);
|
||||
CHECK(HangWatchdog::directoryFor(application) == std::filesystem::path("/tmp/custom-hangs"));
|
||||
unsetenv("MODULES_HANG_DIR");
|
||||
}
|
||||
}
|
||||
231
GraphicApplication/tests/InputCoalescerTests.cpp
Normal file
231
GraphicApplication/tests/InputCoalescerTests.cpp
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
#include "InputCoalescer.hpp"
|
||||
#include "InputEventSink.hpp"
|
||||
|
||||
#include "UnitTest++/UnitTest++.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace tp;
|
||||
|
||||
namespace {
|
||||
class Recorder : public InputEventSink {
|
||||
public:
|
||||
void onPointerSample(const PointerSample& sample) override {
|
||||
log.push_back("sample " + std::to_string((int) std::lround(sample.pressure * 100)));
|
||||
}
|
||||
void onPointerMoved(Vec2F position) override { log.push_back("move " + point(position)); }
|
||||
void onPointerLeft() override { log.push_back("leave"); }
|
||||
void onPointerButton(InputID button, bool pressed) override {
|
||||
log.push_back(std::string(pressed ? "press " : "release ") + std::to_string((int) button));
|
||||
}
|
||||
void onScrolled(Vec2F delta, ScrollSource source) override {
|
||||
log.push_back(std::string(source == ScrollSource::Touchpad ? "touchpad " : "scroll ") + point(delta));
|
||||
}
|
||||
void onScrollEnded() override { log.push_back("scroll end"); }
|
||||
void onKey(InputID key, bool pressed) override {
|
||||
log.push_back(std::string(pressed ? "key down " : "key up ") + std::to_string((int) key));
|
||||
}
|
||||
void onKeyRepeat(InputID key) override { log.push_back("repeat " + std::to_string((int) key)); }
|
||||
void onText(const std::string& utf8) override { log.push_back("text " + utf8); }
|
||||
void onKeyboardFocus(bool focused) override { log.push_back(focused ? "focus" : "blur"); }
|
||||
|
||||
std::vector<std::string> log;
|
||||
|
||||
private:
|
||||
static std::string point(Vec2F value) {
|
||||
return std::to_string((int) value.x) + "," + std::to_string((int) value.y);
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::string> drain(InputCoalescer& coalescer) {
|
||||
Recorder recorder;
|
||||
coalescer.deliver(recorder);
|
||||
return recorder.log;
|
||||
}
|
||||
}
|
||||
|
||||
SUITE(InputCoalescerPolicies) {
|
||||
TEST(OppositeScrollsThatCancelDeliverNothing) {
|
||||
InputCoalescer coalescer;
|
||||
coalescer.scrolled({ 0, 1 });
|
||||
coalescer.scrolled({ 0, -1 });
|
||||
CHECK(drain(coalescer).empty());
|
||||
}
|
||||
|
||||
TEST(BurstOfMotionCollapsesToLatestPosition) {
|
||||
InputCoalescer coalescer;
|
||||
coalescer.pointerMoved({ 1, 1 });
|
||||
coalescer.pointerMoved({ 2, 2 });
|
||||
coalescer.pointerMoved({ 3, 3 });
|
||||
|
||||
const std::vector<std::string> log = drain(coalescer);
|
||||
REQUIRE CHECK_EQUAL(1u, log.size());
|
||||
CHECK_EQUAL("move 3,3", log[0]);
|
||||
CHECK_EQUAL(3u, coalescer.statistics().received);
|
||||
CHECK_EQUAL(1u, coalescer.statistics().delivered);
|
||||
}
|
||||
|
||||
TEST(EveryPolicyKeepsEachMotionSampleForDrawingApps) {
|
||||
InputCoalescer coalescer;
|
||||
coalescer.setPolicies({ DeliveryPolicy::Every, DeliveryPolicy::Accumulate });
|
||||
coalescer.pointerMoved({ 1, 1 });
|
||||
coalescer.pointerMoved({ 2, 2 });
|
||||
coalescer.pointerMoved({ 3, 3 });
|
||||
CHECK_EQUAL(3u, drain(coalescer).size());
|
||||
}
|
||||
|
||||
TEST(MotionNeverMergesAcrossButtons) {
|
||||
InputCoalescer coalescer;
|
||||
coalescer.pointerMoved({ 1, 1 });
|
||||
coalescer.pointerMoved({ 5, 5 });
|
||||
coalescer.pointerButton(InputID::MOUSE1, true);
|
||||
coalescer.pointerMoved({ 6, 6 });
|
||||
coalescer.pointerMoved({ 9, 9 });
|
||||
coalescer.pointerButton(InputID::MOUSE1, false);
|
||||
|
||||
const std::vector<std::string> log = drain(coalescer);
|
||||
REQUIRE CHECK_EQUAL(4u, log.size());
|
||||
CHECK_EQUAL("move 5,5", log[0]);
|
||||
CHECK_EQUAL("press 501", log[1]);
|
||||
CHECK_EQUAL("move 9,9", log[2]);
|
||||
CHECK_EQUAL("release 501", log[3]);
|
||||
}
|
||||
|
||||
TEST(ScrollDeltasAddUpAndDoNotCrossKeys) {
|
||||
InputCoalescer coalescer;
|
||||
coalescer.scrolled({ 0, 1 });
|
||||
coalescer.scrolled({ 0, 2 });
|
||||
coalescer.key(InputID::LEFT_CONTROL, true);
|
||||
coalescer.scrolled({ 0, -1 });
|
||||
|
||||
const std::vector<std::string> log = drain(coalescer);
|
||||
REQUIRE CHECK_EQUAL(3u, log.size());
|
||||
CHECK_EQUAL("scroll 0,3", log[0]);
|
||||
CHECK_EQUAL("key down " + std::to_string((int) InputID::LEFT_CONTROL), log[1]);
|
||||
CHECK_EQUAL("scroll 0,-1", log[2]);
|
||||
}
|
||||
|
||||
TEST(TouchpadScrollsKeepTheirSourceAndEnd) {
|
||||
InputCoalescer coalescer;
|
||||
coalescer.scrolled({ 0, 1 }, ScrollSource::Touchpad);
|
||||
coalescer.scrolled({ 0, 2 }, ScrollSource::Touchpad);
|
||||
coalescer.scrollEnded();
|
||||
|
||||
const std::vector<std::string> log = drain(coalescer);
|
||||
REQUIRE CHECK_EQUAL(2u, log.size());
|
||||
CHECK_EQUAL("touchpad 0,3", log[0]);
|
||||
CHECK_EQUAL("scroll end", log[1]);
|
||||
}
|
||||
|
||||
TEST(WheelAndTouchpadScrollsAreNotMergedTogether) {
|
||||
InputCoalescer coalescer;
|
||||
coalescer.scrolled({ 0, 1 }, ScrollSource::Wheel);
|
||||
coalescer.scrolled({ 0, 2 }, ScrollSource::Touchpad);
|
||||
|
||||
const std::vector<std::string> log = drain(coalescer);
|
||||
REQUIRE CHECK_EQUAL(2u, log.size());
|
||||
CHECK_EQUAL("scroll 0,1", log[0]);
|
||||
CHECK_EQUAL("touchpad 0,2", log[1]);
|
||||
}
|
||||
|
||||
TEST(ScrollsOnEitherSideOfAnEndAreNotMerged) {
|
||||
InputCoalescer coalescer;
|
||||
coalescer.scrolled({ 0, 1 }, ScrollSource::Touchpad);
|
||||
coalescer.scrollEnded();
|
||||
coalescer.scrolled({ 0, 1 }, ScrollSource::Touchpad);
|
||||
CHECK_EQUAL(3u, drain(coalescer).size());
|
||||
}
|
||||
|
||||
TEST(DiscreteEventsAreNeverMerged) {
|
||||
InputCoalescer coalescer;
|
||||
coalescer.key(InputID::A, true);
|
||||
coalescer.key(InputID::A, false);
|
||||
coalescer.key(InputID::A, true);
|
||||
coalescer.keyRepeat(InputID::A);
|
||||
coalescer.keyRepeat(InputID::A);
|
||||
coalescer.text("a");
|
||||
coalescer.text("b");
|
||||
coalescer.keyboardFocus(false);
|
||||
coalescer.pointerLeft();
|
||||
CHECK_EQUAL(9u, drain(coalescer).size());
|
||||
}
|
||||
|
||||
TEST(DeliveryEmptiesTheQueueAndKeepsOrder) {
|
||||
InputCoalescer coalescer;
|
||||
coalescer.text("x");
|
||||
coalescer.pointerMoved({ 4, 4 });
|
||||
const std::vector<std::string> log = drain(coalescer);
|
||||
REQUIRE CHECK_EQUAL(2u, log.size());
|
||||
CHECK_EQUAL("text x", log[0]);
|
||||
CHECK_EQUAL("move 4,4", log[1]);
|
||||
CHECK(coalescer.isEmpty());
|
||||
CHECK_EQUAL(0u, drain(coalescer).size());
|
||||
}
|
||||
|
||||
TEST(OnlyTheNewestConfigureIsApplied) {
|
||||
InputCoalescer coalescer;
|
||||
CHECK(!coalescer.hasPendingConfigure());
|
||||
coalescer.configure({ 800, 600, false, false, true, false, 10 });
|
||||
coalescer.configure({ 820, 610, false, false, true, false, 11 });
|
||||
coalescer.configure({ 900, 640, true, false, true, false, 12 });
|
||||
CHECK(coalescer.hasPendingConfigure());
|
||||
|
||||
const WindowConfigure latest = coalescer.takeConfigure();
|
||||
CHECK_EQUAL(900, latest.width);
|
||||
CHECK_EQUAL(640, latest.height);
|
||||
CHECK(latest.maximized);
|
||||
CHECK_EQUAL(12u, latest.serial);
|
||||
CHECK(!coalescer.hasPendingConfigure());
|
||||
CHECK_EQUAL(3u, coalescer.statistics().configuresReceived);
|
||||
CHECK_EQUAL(1u, coalescer.statistics().configuresApplied);
|
||||
}
|
||||
}
|
||||
|
||||
SUITE(InputCoalescerPenSamples) {
|
||||
PointerSample pen(halnf pressure) { return PointerSample{ PointerDevice::Stylus, StylusTip::Pen, pressure }; }
|
||||
|
||||
TEST(EachDeliveredMotionCarriesThePressureItWasRecordedWith) {
|
||||
InputCoalescer coalescer;
|
||||
coalescer.setPolicies({ DeliveryPolicy::Every, DeliveryPolicy::Accumulate });
|
||||
coalescer.pointerSample(pen(0.25f));
|
||||
coalescer.pointerMoved({ 1, 1 });
|
||||
coalescer.pointerSample(pen(0.75f));
|
||||
coalescer.pointerMoved({ 2, 2 });
|
||||
|
||||
const std::vector<std::string> log = drain(coalescer);
|
||||
REQUIRE CHECK_EQUAL(4u, log.size());
|
||||
CHECK_EQUAL("sample 25", log[0]);
|
||||
CHECK_EQUAL("move 1,1", log[1]);
|
||||
CHECK_EQUAL("sample 75", log[2]);
|
||||
CHECK_EQUAL("move 2,2", log[3]);
|
||||
}
|
||||
|
||||
TEST(LatestMotionKeepsTheNewestSample) {
|
||||
InputCoalescer coalescer;
|
||||
coalescer.pointerSample(pen(0.2f));
|
||||
coalescer.pointerMoved({ 1, 1 });
|
||||
coalescer.pointerSample(pen(0.9f));
|
||||
coalescer.pointerMoved({ 4, 4 });
|
||||
|
||||
const std::vector<std::string> log = drain(coalescer);
|
||||
REQUIRE CHECK_EQUAL(2u, log.size());
|
||||
CHECK_EQUAL("sample 90", log[0]);
|
||||
CHECK_EQUAL("move 4,4", log[1]);
|
||||
}
|
||||
|
||||
TEST(UnchangedSampleIsNotRepeated) {
|
||||
InputCoalescer coalescer;
|
||||
coalescer.setPolicies({ DeliveryPolicy::Every, DeliveryPolicy::Accumulate });
|
||||
coalescer.pointerSample(pen(0.5f));
|
||||
coalescer.pointerMoved({ 1, 1 });
|
||||
coalescer.pointerButton(InputID::MOUSE1, true);
|
||||
coalescer.pointerMoved({ 2, 2 });
|
||||
|
||||
const std::vector<std::string> log = drain(coalescer);
|
||||
REQUIRE CHECK_EQUAL(4u, log.size());
|
||||
CHECK_EQUAL("sample 50", log[0]);
|
||||
CHECK_EQUAL("move 2,2", log[3]);
|
||||
}
|
||||
}
|
||||
180
GraphicApplication/tests/InputRecordingTests.cpp
Normal file
180
GraphicApplication/tests/InputRecordingTests.cpp
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
#include "InputCoalescer.hpp"
|
||||
#include "InputRecording.hpp"
|
||||
|
||||
#include "UnitTest++/UnitTest++.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
using namespace tp;
|
||||
|
||||
namespace {
|
||||
struct ManualClock {
|
||||
double now = 0;
|
||||
InputRecorder::Clock clock() {
|
||||
return [this]() { return now; };
|
||||
}
|
||||
};
|
||||
|
||||
struct Collector : InputEventSink {
|
||||
std::vector<std::string> calls;
|
||||
void onPointerSample(const PointerSample& sample) override {
|
||||
calls.push_back("sample " + std::to_string(sample.pressure));
|
||||
}
|
||||
void onPointerMoved(Vec2F position) override {
|
||||
calls.push_back("move " + std::to_string((int) position.x) + " " + std::to_string((int) position.y));
|
||||
}
|
||||
void onPointerButton(InputID, bool pressed) override { calls.push_back(pressed ? "down" : "up"); }
|
||||
void onKey(InputID, bool pressed) override { calls.push_back(pressed ? "key down" : "key up"); }
|
||||
void onScrolled(Vec2F delta, ScrollSource source) override {
|
||||
calls.push_back(
|
||||
std::string(source == ScrollSource::Touchpad ? "touchpad " : "wheel ") + std::to_string((int) delta.y)
|
||||
);
|
||||
}
|
||||
void onScrollEnded() override { calls.push_back("scroll end"); }
|
||||
};
|
||||
|
||||
InputRecording penStroke() {
|
||||
ManualClock time;
|
||||
InputRecorder recorder(time.clock());
|
||||
recorder.setWindowSize({ 1600, 1000 });
|
||||
PointerSample pen;
|
||||
pen.device = PointerDevice::Stylus;
|
||||
pen.pressure = 0.25f;
|
||||
pen.tilt = { 0.1f, -0.2f };
|
||||
pen.timeMs = 77;
|
||||
|
||||
time.now = 1000;
|
||||
recorder.onKey(InputID::LEFT_SHIFT, true);
|
||||
time.now = 1010;
|
||||
recorder.onPointerSample(pen);
|
||||
recorder.onPointerMoved({ 10, 20 });
|
||||
recorder.onPointerButton(InputID::MOUSE1, true);
|
||||
time.now = 1012.5;
|
||||
recorder.onPointerMoved({ 30, 40 });
|
||||
time.now = 1030;
|
||||
recorder.onPointerButton(InputID::MOUSE1, false);
|
||||
time.now = 1200;
|
||||
recorder.onKey(InputID::LEFT_SHIFT, false);
|
||||
return recorder.recording();
|
||||
}
|
||||
}
|
||||
|
||||
SUITE(InputRecording) {
|
||||
TEST(RecorderTimesEventsFromTheFirstOne) {
|
||||
const InputRecording recording = penStroke();
|
||||
REQUIRE CHECK_EQUAL(7u, recording.events.size());
|
||||
CHECK_EQUAL(0.0, recording.events[0].timeMs);
|
||||
CHECK_EQUAL(12.5, recording.events[4].timeMs);
|
||||
CHECK_EQUAL(200.0, recording.durationMs());
|
||||
}
|
||||
|
||||
TEST(ExplicitClockStartTimesTheFirstEventFromThere) {
|
||||
ManualClock time;
|
||||
InputRecorder recorder(time.clock());
|
||||
time.now = 500;
|
||||
recorder.startClockNow();
|
||||
time.now = 740;
|
||||
recorder.onPointerMoved({ 1, 1 });
|
||||
CHECK_EQUAL(240.0, recorder.recording().events.front().timeMs);
|
||||
}
|
||||
|
||||
TEST(SerializedRecordingParsesBackIdentically) {
|
||||
const InputRecording recording = penStroke();
|
||||
const std::optional<InputRecording> parsed = InputRecording::parse(recording.serialize());
|
||||
REQUIRE CHECK(parsed.has_value());
|
||||
CHECK_EQUAL(1600.0f, parsed->windowSize.x);
|
||||
REQUIRE CHECK_EQUAL(recording.events.size(), parsed->events.size());
|
||||
for (size_t index = 0; index < recording.events.size(); index++) {
|
||||
CHECK(recording.events[index] == parsed->events[index]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TouchpadScrollsAndTheirEndRoundTripAndReplay) {
|
||||
ManualClock time;
|
||||
InputRecorder recorder(time.clock());
|
||||
recorder.onScrolled({ 0, 2 }, ScrollSource::Wheel);
|
||||
time.now = 5;
|
||||
recorder.onScrolled({ 0, -3 }, ScrollSource::Touchpad);
|
||||
time.now = 9;
|
||||
recorder.onScrollEnded();
|
||||
|
||||
const std::optional<InputRecording> parsed = InputRecording::parse(recorder.recording().serialize());
|
||||
REQUIRE CHECK(parsed.has_value());
|
||||
CHECK(parsed->events == recorder.recording().events);
|
||||
|
||||
Collector collector;
|
||||
for (const RecordedInput& event : parsed->events) {
|
||||
event.deliverTo(collector);
|
||||
}
|
||||
CHECK((std::vector<std::string>{ "wheel 2", "touchpad -3", "scroll end" } == collector.calls));
|
||||
}
|
||||
|
||||
TEST(OldRecordingsScrollAsAWheel) {
|
||||
const std::optional<InputRecording> parsed =
|
||||
InputRecording::parse(std::string(InputRecording::kHeader) + "\n0.000 scroll 0.000 1.000\n");
|
||||
REQUIRE CHECK(parsed.has_value());
|
||||
Collector collector;
|
||||
parsed->events.front().deliverTo(collector);
|
||||
CHECK((std::vector<std::string>{ "wheel 1" } == collector.calls));
|
||||
}
|
||||
|
||||
TEST(OldRecordingsKeepTheirPointerTool) {
|
||||
const std::string header = std::string(InputRecording::kHeader) + "\n";
|
||||
const auto toolOf = [&](int legacyTool) {
|
||||
const std::string line = "0.000 sample " + std::to_string(legacyTool) + " 0.5 0 0 0 0 0\n";
|
||||
const std::optional<InputRecording> parsed = InputRecording::parse(header + line);
|
||||
return parsed ? std::optional<PointerSample>(parsed->events.front().sample) : std::nullopt;
|
||||
};
|
||||
|
||||
CHECK(toolOf(0)->device == PointerDevice::Mouse);
|
||||
CHECK(toolOf(1)->device == PointerDevice::Stylus && toolOf(1)->tip == StylusTip::Pen);
|
||||
CHECK(toolOf(2)->device == PointerDevice::Stylus && toolOf(2)->tip == StylusTip::Eraser);
|
||||
CHECK(toolOf(5)->tip == StylusTip::Airbrush);
|
||||
CHECK(toolOf(6)->device == PointerDevice::Finger);
|
||||
CHECK(toolOf(7)->device == PointerDevice::Mouse);
|
||||
CHECK(!toolOf(8).has_value());
|
||||
}
|
||||
|
||||
TEST(ParseRejectsForeignText) {
|
||||
CHECK(!InputRecording::parse("hello\n").has_value());
|
||||
CHECK(!InputRecording::parse(std::string(InputRecording::kHeader) + "\n1.0 teleport 3\n").has_value());
|
||||
}
|
||||
|
||||
TEST(ReplayDeliversEventsInOrder) {
|
||||
Collector collector;
|
||||
for (const RecordedInput& event : penStroke().events) {
|
||||
event.deliverTo(collector);
|
||||
}
|
||||
const std::vector<std::string> expected = { "key down", "sample 0.250000", "move 10 20", "down", "move 30 40",
|
||||
"up", "key up" };
|
||||
CHECK(expected == collector.calls);
|
||||
}
|
||||
|
||||
TEST(SliceKeepsThePointerActivityAndRebasesTime) {
|
||||
const InputRecording sliced = penStroke().slicedToPointerActivity();
|
||||
REQUIRE CHECK_EQUAL(5u, sliced.events.size());
|
||||
CHECK(sliced.events.front().kind == RecordedInput::Kind::Sample);
|
||||
CHECK_EQUAL(0.0, sliced.events.front().timeMs);
|
||||
CHECK(sliced.events.back().kind == RecordedInput::Kind::Button);
|
||||
CHECK_EQUAL(20.0, sliced.events.back().timeMs);
|
||||
}
|
||||
|
||||
TEST(TranslateMovesOnlyPointerPositions) {
|
||||
const InputRecording moved = penStroke().translated({ 5, -5 });
|
||||
CHECK_EQUAL(15.0f, moved.events[2].vector.x);
|
||||
CHECK_EQUAL(15.0f, moved.events[2].vector.y);
|
||||
CHECK_EQUAL(0.25f, moved.events[1].sample.pressure);
|
||||
}
|
||||
|
||||
TEST(CoalescerTapSeesEveryRawEventBeforeCoalescing) {
|
||||
InputCoalescer coalescer;
|
||||
Collector tap;
|
||||
coalescer.setTap(&tap);
|
||||
coalescer.recorder().onPointerMoved({ 1, 1 });
|
||||
coalescer.recorder().onPointerMoved({ 2, 2 });
|
||||
coalescer.recorder().onPointerMoved({ 3, 3 });
|
||||
|
||||
CHECK_EQUAL(3u, tap.calls.size());
|
||||
CHECK_EQUAL(1u, coalescer.pendingEvents());
|
||||
}
|
||||
}
|
||||
94
GraphicApplication/tests/InputStateTests.cpp
Normal file
94
GraphicApplication/tests/InputStateTests.cpp
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#include "InputState.hpp"
|
||||
|
||||
#include "UnitTest++/UnitTest++.h"
|
||||
|
||||
using namespace tp;
|
||||
|
||||
SUITE(InputState) {
|
||||
TEST(AllInputKindsArriveInOneOrderedBatch) {
|
||||
InputState input;
|
||||
TouchGesture gesture;
|
||||
gesture.scale = 1.5f;
|
||||
|
||||
input.onText("a");
|
||||
input.onKeyRepeat(InputID::A);
|
||||
input.onGesture(gesture);
|
||||
input.onKeyboardFocus(false);
|
||||
input.onResized();
|
||||
|
||||
CHECK_EQUAL(5u, input.pendingEventCount());
|
||||
input.applyPendingEvents();
|
||||
CHECK_EQUAL(0u, input.pendingEventCount());
|
||||
|
||||
const std::vector<InputEvent>& events = input.events();
|
||||
REQUIRE CHECK_EQUAL(5u, events.size());
|
||||
CHECK(events[0].type == InputEvent::Type::TEXT);
|
||||
CHECK_EQUAL(std::string("a"), events[0].text);
|
||||
CHECK(events[1].type == InputEvent::Type::BUTTON_ACTION);
|
||||
CHECK(events[1].buttonAction == InputEvent::ButtonAction::REPEAT);
|
||||
CHECK(events[1].id == InputID::A);
|
||||
CHECK(events[2].type == InputEvent::Type::GESTURE);
|
||||
CHECK_CLOSE(1.5f, events[2].gesture.scale, 0.0001f);
|
||||
CHECK(events[3].type == InputEvent::Type::FOCUS);
|
||||
CHECK(!events[3].focused);
|
||||
CHECK(events[4].type == InputEvent::Type::RESIZED);
|
||||
}
|
||||
|
||||
TEST(PressAndReleaseInOneBatchRemainVisible) {
|
||||
InputState input;
|
||||
input.onPointerButton(InputID::MOUSE1, true);
|
||||
input.onPointerButton(InputID::MOUSE1, false);
|
||||
input.applyPendingEvents();
|
||||
|
||||
CHECK(input.isPressed(InputID::MOUSE1));
|
||||
CHECK(input.isReleased(InputID::MOUSE1));
|
||||
CHECK(!input.isDown(InputID::MOUSE1));
|
||||
|
||||
input.applyPendingEvents();
|
||||
CHECK(!input.isPressed(InputID::MOUSE1));
|
||||
CHECK(!input.isReleased(InputID::MOUSE1));
|
||||
}
|
||||
|
||||
TEST(PointerAndScrollDescribeTheWholeBatch) {
|
||||
InputState input;
|
||||
input.onPointerMoved({ 10, 20 });
|
||||
input.onPointerMoved({ 15, 25 });
|
||||
input.onScrolled({ 1, 2 }, ScrollSource::Wheel);
|
||||
input.onScrolled({ 3, 4 }, ScrollSource::Touchpad);
|
||||
input.applyPendingEvents();
|
||||
|
||||
CHECK_CLOSE(15, input.getPointer().x, 0.0001f);
|
||||
CHECK_CLOSE(25, input.getPointer().y, 0.0001f);
|
||||
CHECK_CLOSE(15, input.getPointerDelta().x, 0.0001f);
|
||||
CHECK_CLOSE(25, input.getPointerDelta().y, 0.0001f);
|
||||
CHECK_CLOSE(4, input.getScrollX(), 0.0001f);
|
||||
CHECK_CLOSE(6, input.getScrollY(), 0.0001f);
|
||||
CHECK(input.isContinuousScroll());
|
||||
|
||||
input.applyPendingEvents();
|
||||
CHECK_CLOSE(0, input.getPointerDelta().x, 0.0001f);
|
||||
CHECK_CLOSE(0, input.getPointerDelta().y, 0.0001f);
|
||||
CHECK_CLOSE(0, input.getScrollX(), 0.0001f);
|
||||
CHECK_CLOSE(0, input.getScrollY(), 0.0001f);
|
||||
CHECK(!input.isContinuousScroll());
|
||||
}
|
||||
|
||||
TEST(FocusLossReleasesHeldKeysBeforeTheFocusEvent) {
|
||||
InputState input;
|
||||
input.onKey(InputID::A, true);
|
||||
input.applyPendingEvents();
|
||||
CHECK(input.isDown(InputID::A));
|
||||
|
||||
input.onKeyboardFocus(false);
|
||||
input.applyPendingEvents();
|
||||
|
||||
CHECK(!input.isDown(InputID::A));
|
||||
CHECK(input.isReleased(InputID::A));
|
||||
const std::vector<InputEvent>& events = input.events();
|
||||
REQUIRE CHECK_EQUAL(2u, events.size());
|
||||
CHECK(events[0].type == InputEvent::Type::BUTTON_ACTION);
|
||||
CHECK(events[0].buttonAction == InputEvent::ButtonAction::RELEASE);
|
||||
CHECK(events[1].type == InputEvent::Type::FOCUS);
|
||||
CHECK(!events[1].focused);
|
||||
}
|
||||
}
|
||||
39
GraphicApplication/tests/KeyMappingTests.cpp
Normal file
39
GraphicApplication/tests/KeyMappingTests.cpp
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#include "UnitTest++/UnitTest++.h"
|
||||
|
||||
#include "KeyMapping.hpp"
|
||||
|
||||
#include <linux/input-event-codes.h>
|
||||
|
||||
using namespace tp;
|
||||
|
||||
SUITE(EvdevKeyMapping) {
|
||||
TEST(LettersAndDigitsKeepTheirInputCodes) {
|
||||
CHECK(inputFromEvdevKey(KEY_A) == InputID::A);
|
||||
CHECK(inputFromEvdevKey(KEY_Z) == InputID::Z);
|
||||
CHECK(inputFromEvdevKey(KEY_0) == InputID::N0);
|
||||
CHECK(inputFromEvdevKey(KEY_9) == InputID::N9);
|
||||
}
|
||||
|
||||
TEST(NavigationAndEditingKeysAreMapped) {
|
||||
CHECK(inputFromEvdevKey(KEY_ESC) == InputID::ESCAPE);
|
||||
CHECK(inputFromEvdevKey(KEY_ENTER) == InputID::ENTER);
|
||||
CHECK(inputFromEvdevKey(KEY_BACKSPACE) == InputID::BACKSPACE);
|
||||
CHECK(inputFromEvdevKey(KEY_LEFT) == InputID::LEFT);
|
||||
CHECK(inputFromEvdevKey(KEY_PAGEDOWN) == InputID::PAGE_DOWN);
|
||||
}
|
||||
|
||||
TEST(ModifiersKeepSides) {
|
||||
CHECK(inputFromEvdevKey(KEY_LEFTCTRL) == InputID::LEFT_CONTROL);
|
||||
CHECK(inputFromEvdevKey(KEY_RIGHTSHIFT) == InputID::RIGHT_SHIFT);
|
||||
CHECK(inputFromEvdevKey(KEY_LEFTALT) == InputID::LEFT_ALT);
|
||||
}
|
||||
|
||||
TEST(UnknownKeysAreIgnored) { CHECK(!inputFromEvdevKey(KEY_MAX).has_value()); }
|
||||
|
||||
TEST(PointerButtonsMapToMouseInputs) {
|
||||
CHECK(inputFromPointerButton(BTN_LEFT) == InputID::MOUSE1);
|
||||
CHECK(inputFromPointerButton(BTN_RIGHT) == InputID::MOUSE2);
|
||||
CHECK(inputFromPointerButton(BTN_MIDDLE) == InputID::MOUSE3);
|
||||
CHECK(!inputFromPointerButton(BTN_TOUCH).has_value());
|
||||
}
|
||||
}
|
||||
3
GraphicApplication/tests/Main.cpp
Normal file
3
GraphicApplication/tests/Main.cpp
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#include "UnitTest++/UnitTest++.h"
|
||||
|
||||
int main() { return UnitTest::RunAllTests(); }
|
||||
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