GraphicApplication: Vulkan renderer, native windows, input and frame diagnostics #9

Open
agent wants to merge 5 commits from GraphicApplication into master
150 changed files with 12488 additions and 1467 deletions

View file

@ -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}/")

View file

@ -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();

View file

@ -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

View file

@ -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)

View file

@ -0,0 +1,4 @@
---
BasedOnStyle: InheritParentConfig
AllowShortIfStatementsOnASingleLine: Never
InsertBraces: true

View 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 ()

View file

@ -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();
}

View 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 = &currentFrame;
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, &region);
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;
}

Binary file not shown.

View 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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.

View 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.

View 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.

View file

@ -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);
}

View 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;
}
}

View file

@ -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;
};
}

View file

@ -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;
};
}

View file

@ -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);
}
}
}
}

View 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;
}
}

View file

@ -0,0 +1,11 @@
#pragma once
#include "InputCodes.hpp"
#include <cstdint>
#include <optional>
namespace tp::android {
[[nodiscard]] std::optional<InputID> inputFromKeyCode(int32_t keyCode);
}

View file

@ -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());
}
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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;
};
}

View file

@ -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
);
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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
);
}

View file

@ -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;
};
}

View file

@ -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;
};
}

View file

@ -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);
}

View file

@ -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);
}
}
}

View file

@ -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;
};
}

File diff suppressed because it is too large Load diff

View file

@ -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;
};
}

View file

@ -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);
}
}

View file

@ -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;
};
}

View 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);
}

View 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 }; }
}

View 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;
}

View 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);
}

View 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);
}

View 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);
}
}

View 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 };
};
}

View 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); }

View 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 });
}

View 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;
};
}

View 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;
};
}

View 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);
}
}
}

View 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);
}
}

View 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;
}
}

View 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();
};
}

View 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;
};
}

View 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)

View 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;
}
}
}

View 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);
}
}

View 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; }

View 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;
}
}

View 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);
}
}

View 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;
};
}

View file

@ -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,

View 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) {}
};
}

View 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;
};
}

View 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;
};
}

View 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);
}

View 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;
};
}

View 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;
};
}

View 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);
}
}
}

View file

@ -0,0 +1,23 @@
#include "RenderTargetRegistry.hpp"
namespace tp {
uint32_t RenderTargetRegistry::registerTarget(const RenderTargetEntry& entry) {
const uint32_t id = mNextId++;
mEntries[id] = entry;
return id;
}
void RenderTargetRegistry::updateTarget(uint32_t id, const RenderTargetEntry& entry) { mEntries[id] = entry; }
void RenderTargetRegistry::unregisterTarget(uint32_t id) { mEntries.erase(id); }
bool RenderTargetRegistry::resolve(uint32_t id, RenderTargetEntry& out) const {
const auto found = mEntries.find(id);
if (found == mEntries.end()) {
return false;
}
out = found->second;
return true;
}
}

View 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);
}
}

View 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;
};
}

View file

@ -0,0 +1,29 @@
#pragma once
#include <vulkan/vulkan.h>
#include <cstdint>
#include <unordered_map>
namespace tp {
struct RenderTargetEntry {
VkImageView view = VK_NULL_HANDLE;
VkSampler sampler = VK_NULL_HANDLE;
int width = 0;
int height = 0;
VkImage image = VK_NULL_HANDLE;
};
class RenderTargetRegistry {
public:
uint32_t registerTarget(const RenderTargetEntry& entry);
void updateTarget(uint32_t id, const RenderTargetEntry& entry);
void unregisterTarget(uint32_t id);
[[nodiscard]] bool resolve(uint32_t id, RenderTargetEntry& out) const;
private:
uint32_t mNextId = 1;
std::unordered_map<uint32_t, RenderTargetEntry> mEntries;
};
}

View 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;
};
}

View 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(); }

View 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;
};
}

View 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);
}
}

View 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();
}

View 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);
}

View 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); }

View 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;
};
}

View 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;
};
}

View 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;
};
}

View 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;
};
}

View 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;
};
}

View 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));
}
}

View 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");
}
}

View 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]);
}
}

View 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());
}
}

View 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);
}
}

View 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());
}
}

View 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