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

Merged
ilusha merged 5 commits from GraphicApplication into master 2026-10-29 10:38:29 +03:00
183 changed files with 12901 additions and 2133 deletions

View file

@ -18,7 +18,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE ${BINDINGS_LIBS})
file(GLOB APP_SOURCES "./applications/*.cpp")
add_executable(3DEditorApp ${APP_SOURCES})
target_link_libraries(3DEditorApp ${PROJECT_NAME})
graphic_application_resources(3DEditorApp)
file(COPY "../RasterRender/rsc" DESTINATION "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/")
file(COPY "../Graphics/rsc" DESTINATION "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/")
file(COPY "../3DScene/rsc" DESTINATION "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/")

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

@ -10,28 +10,28 @@ namespace tp {
public:
explicit ViewportWidget(Canvas* canvas, Editor* editor) {
mEditor = editor;
mImage = canvas->createImageFromTextId(mEditor->getViewportTexID(), { 0, 0 });
mImage = canvas->createImageFromRenderTarget(mEditor->getViewportTexID());
mCanvas = canvas;
}
~ViewportWidget() override { mCanvas->deleteImageHandle(mImage); }
~ViewportWidget() override { mCanvas->deleteImage(mImage); }
void process(const EventHandler& events) override {
void process(const InputView& events) override {
mEditor->setViewportSize(getAreaT().size);
}
void draw(Canvas& canvas) override {
mEditor->renderViewport();
canvas.updateTextureID(mImage, mEditor->getViewportTexID());
canvas.drawImage(getArea().relative(), &mImage, PI);
canvas.updateImageFromRenderTarget(mImage, mEditor->getViewportTexID());
canvas.drawImage(getArea().relative(), mImage, PI);
}
public:
Editor* mEditor;
Canvas* mCanvas = nullptr;
Canvas::ImageHandle mImage;
ImageHandle mImage;
};
class EditorWidget : public DockWidget {
@ -91,7 +91,7 @@ namespace tp {
mNavigationReset.setAction( [this]() { mEditor->navigationReset(); });
}
void process(const EventHandler& events) override {
void process(const InputView& events) override {
DockWidget::process(events);
if (auto obj = mEditor->getActiveObject()) { // dummy rotate active object
@ -107,7 +107,7 @@ namespace tp {
auto pointerPrev = events.getPointerPrev();
auto pointerRelative = (((pointer - activeArea.pos) / activeArea.size) - 0.5f) * 2;
if (activeArea.isInside(pointer) && events.isDown(InputID::MOUSE1)) {
if (activeArea.isInside(pointer) && events.isDown(PointerButton::Left)) {
switch (mNavigationType) {
case ORBIT: mEditor->navigationOrbit(events.getPointerDelta() / activeArea.size * 3); break;

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,133 @@
project(GraphicApplication)
find_package(Vulkan REQUIRED)
find_package(Threads REQUIRED)
function(graphic_application_library TARGET_NAME LIBRARY_DIR)
set(DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/${LIBRARY_DIR})
file(GLOB SOURCES CONFIGURE_DEPENDS ${DIR}/private/*.cpp ${DIR}/private/*.hpp ${DIR}/public/*.hpp)
add_library(${TARGET_NAME} STATIC ${SOURCES})
if (EXISTS ${DIR}/public)
target_include_directories(${TARGET_NAME} PUBLIC ${DIR}/public)
endif ()
target_include_directories(${TARGET_NAME} PRIVATE ${DIR}/private)
endfunction()
### ---------------------- Input layer --------------------- ###
graphic_application_library(${PROJECT_NAME}InputLayer input_layer)
target_link_libraries(${PROJECT_NAME}InputLayer PUBLIC Math Modules)
### ---------------------- Diagnostics --------------------- ###
graphic_application_library(${PROJECT_NAME}Diagnostics diagnostics)
target_link_libraries(${PROJECT_NAME}Diagnostics PUBLIC Callstack Threads::Threads)
### ---------------------- GPU device and window surfaces --------------------- ###
graphic_application_library(${PROJECT_NAME}Gpu gpu)
target_link_libraries(${PROJECT_NAME}Gpu PUBLIC Vulkan::Vulkan PRIVATE ${PROJECT_NAME}Diagnostics)
### ---------------------- Wayland protocols --------------------- ###
if (NOT ANDROID)
find_package(PkgConfig REQUIRED)
pkg_check_modules(WAYLAND REQUIRED IMPORTED_TARGET wayland-client xkbcommon)
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
pkg_get_variable(WAYLAND_SCANNER wayland-scanner wayland_scanner)
### ---------------------- Wayland Protocols --------------------- ###
set(WAYLAND_PROTOCOL_FILES
stable/xdg-shell/xdg-shell.xml
stable/viewporter/viewporter.xml
staging/fractional-scale/fractional-scale-v1.xml
staging/cursor-shape/cursor-shape-v1.xml
staging/xdg-activation/xdg-activation-v1.xml
stable/tablet/tablet-v2.xml
unstable/text-input/text-input-unstable-v3.xml
unstable/xdg-decoration/xdg-decoration-unstable-v1.xml
unstable/pointer-gestures/pointer-gestures-unstable-v1.xml
)
set(WAYLAND_GENERATED_DIR ${CMAKE_CURRENT_BINARY_DIR}/wayland)
file(MAKE_DIRECTORY ${WAYLAND_GENERATED_DIR})
foreach(PROTOCOL_FILE ${WAYLAND_PROTOCOL_FILES})
get_filename_component(PROTOCOL_NAME ${PROTOCOL_FILE} NAME_WE)
set(PROTOCOL_XML ${WAYLAND_PROTOCOLS_DIR}/${PROTOCOL_FILE})
set(PROTOCOL_HEADER ${WAYLAND_GENERATED_DIR}/${PROTOCOL_NAME}-client-protocol.h)
set(PROTOCOL_SOURCE ${WAYLAND_GENERATED_DIR}/${PROTOCOL_NAME}-protocol.c)
add_custom_command(
OUTPUT ${PROTOCOL_HEADER} ${PROTOCOL_SOURCE}
COMMAND ${WAYLAND_SCANNER} client-header ${PROTOCOL_XML} ${PROTOCOL_HEADER}
COMMAND ${WAYLAND_SCANNER} private-code ${PROTOCOL_XML} ${PROTOCOL_SOURCE}
DEPENDS ${PROTOCOL_XML}
)
list(APPEND WAYLAND_PROTOCOL_SOURCES ${PROTOCOL_HEADER} ${PROTOCOL_SOURCE})
endforeach()
add_library(WaylandProtocols STATIC ${WAYLAND_PROTOCOL_SOURCES})
target_include_directories(WaylandProtocols PUBLIC ${WAYLAND_GENERATED_DIR})
target_link_libraries(WaylandProtocols PUBLIC PkgConfig::WAYLAND)
set_target_properties(WaylandProtocols PROPERTIES LINKER_LANGUAGE C)
endif ()
### ---------------------- Backend contract --------------------- ###
graphic_application_library(${PROJECT_NAME}BackendContract backends/contract)
target_link_libraries(${PROJECT_NAME}BackendContract PUBLIC Math ${PROJECT_NAME}InputLayer ${PROJECT_NAME}Gpu)
### ---------------------- Platform backend --------------------- ###
if (ANDROID)
graphic_application_library(${PROJECT_NAME}Backend backends/android)
target_link_libraries(${PROJECT_NAME}Backend PUBLIC native_app_glue PRIVATE android log)
else ()
graphic_application_library(${PROJECT_NAME}Backend backends/wayland)
target_link_libraries(${PROJECT_NAME}Backend PRIVATE WaylandProtocols PkgConfig::WAYLAND)
endif ()
target_link_libraries(${PROJECT_NAME}Backend PUBLIC ${PROJECT_NAME}BackendContract)
### ---------------------- Window --------------------- ###
graphic_application_library(${PROJECT_NAME}Window window)
target_link_libraries(${PROJECT_NAME}Window PUBLIC Math ${PROJECT_NAME}BackendContract PRIVATE ${PROJECT_NAME}Backend)
### ---------------------- Canvas --------------------- ###
graphic_application_library(${PROJECT_NAME}Canvas canvas)
target_link_libraries(${PROJECT_NAME}Canvas PUBLIC Math Containers ${PROJECT_NAME}BackendContract PRIVATE ${PROJECT_NAME}Diagnostics NanovgVk)
### ---------------------- Window decorations --------------------- ###
graphic_application_library(${PROJECT_NAME}Decorations decorations)
target_link_libraries(${PROJECT_NAME}Decorations PUBLIC ${PROJECT_NAME}Canvas)
### ---------------------- Debug UI --------------------- ###
graphic_application_library(${PROJECT_NAME}DebugUI debug_ui)
target_link_libraries(${PROJECT_NAME}DebugUI PUBLIC ${PROJECT_NAME}Window PRIVATE Imgui)
### ---------------------- Application --------------------- ###
graphic_application_library(${PROJECT_NAME} application)
target_link_libraries(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}Window ${PROJECT_NAME}Canvas ${PROJECT_NAME}Decorations ${PROJECT_NAME}DebugUI ${PROJECT_NAME}Diagnostics)
target_compile_definitions(${PROJECT_NAME} PRIVATE MODULES_GENERIC_APP_ID="online.ilushabasic.modules" MODULES_GENERIC_APP_NAME="Modules")
# Copies the shared fonts into an "rsc" directory next to an executable, where the desktop backend looks for them.
set(GRAPHIC_APPLICATION_RESOURCES ${CMAKE_CURRENT_SOURCE_DIR}/rsc CACHE INTERNAL "")
function(graphic_application_resources TARGET_NAME)
add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory ${GRAPHIC_APPLICATION_RESOURCES} $<TARGET_FILE_DIR:${TARGET_NAME}>/rsc
)
endfunction()
### -------------------------- Examples -------------------------- ###
if (NOT ANDROID)
add_executable(Example${PROJECT_NAME} ./examples/Example.cpp)
target_link_libraries(Example${PROJECT_NAME} ${PROJECT_NAME} Imgui)
graphic_application_resources(Example${PROJECT_NAME})
add_executable(OffscreenProof ./examples/OffscreenProof.cpp)
target_link_libraries(OffscreenProof NanovgVk Vulkan::Vulkan)
endif ()
### -------------------------- Tests -------------------------- ###
if (NOT ANDROID)
file(GLOB TEST_SOURCES CONFIGURE_DEPENDS "./tests/*.cpp")
add_executable(Test${PROJECT_NAME} ${TEST_SOURCES})
target_include_directories(Test${PROJECT_NAME} PRIVATE ./src/canvas/private ./src/backends/wayland/private)
target_compile_definitions(Test${PROJECT_NAME} PRIVATE MODULES_SOURCE_DIR="${CMAKE_SOURCE_DIR}")
target_link_libraries(Test${PROJECT_NAME} ${PROJECT_NAME}Window ${PROJECT_NAME}Backend ${PROJECT_NAME}Diagnostics NanovgVk UnitTest++)
add_test(NAME Test${PROJECT_NAME} COMMAND Test${PROJECT_NAME})
endif ()

View file

@ -0,0 +1,25 @@
#include "GraphicApplication.hpp"
#include "imgui.h"
using namespace tp;
class ExampleDelegate : public ApplicationDelegate {
public:
void attach(Application& application) { mApplication = &application; }
void drawFrame(Canvas&) override {
ImGui::ShowDemoWindow();
mApplication->showFrameRates();
}
private:
Application* mApplication = nullptr;
};
int main() {
ExampleDelegate delegate;
Application application(delegate);
delegate.attach(application);
application.run();
}

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,175 @@
#include "GraphicApplication.hpp"
using namespace tp;
WindowIdentity WindowIdentity::generic() { return { MODULES_GENERIC_APP_ID, MODULES_GENERIC_APP_NAME }; }
Application::Application(ApplicationDelegate& delegate, const ApplicationOptions& options) :
mDelegate(delegate),
mProcessSection(mProfiler.section("frame.process")),
mPresentSection(mProfiler.section("frame.present")),
mDrawSection(mProfiler.section("frame.draw")),
mInputEventsCounter(mProfiler.counter("input.events")),
mGpu(Window::createGpuDevice()),
mWindow(std::make_unique<Window>(*mGpu, mProfiler, options.window)),
mCanvas(std::make_unique<Canvas>(mWindow->surface(), mProfiler, options.resourceDirectory)),
mDebugGUI(std::make_unique<DebugGUI>(*mWindow, options.resourceDirectory, options.debugInterface)) {
mHangSettings.application = HangWatchdog::processName();
mPerSecondTimer.setDuration(1000.f);
}
Application::~Application() { finish(); }
void Application::setHangSettings(HangSettings settings) { mHangSettings = std::move(settings); }
void Application::start() {
if (mRunning) {
return;
}
mHangWatchdog = std::make_unique<HangWatchdog>(mHangSettings, mProfiler.trail());
for (const HangWatchdog::ReportFileSource& reportFileSource : mHangReportFileSources) {
mHangWatchdog->addReportFileSource(reportFileSource);
}
mWindow->loop().waitForReady();
mPreviousFrameTime = get_time();
mPerSecondTimer.reset();
mRunning = true;
runFrame();
}
void Application::step(halnf deltaMs) {
if (!mRunning) {
return;
}
enterPhase(UiPhase::Idle);
mWindow->loop().processEvents(false);
runFrame(deltaMs);
}
void Application::finish() {
if (!mRunning) {
return;
}
mHangWatchdog.reset();
mRunning = false;
}
void Application::run() {
start();
WindowEventLoop& loop = mWindow->loop();
while (mRunning && !mWindow->controls().shouldClose()) {
enterPhase(UiPhase::Idle);
loop.processEvents(!wantsFrame() || loop.isWaitingForFrame());
while (loop.isWaitingForFrame() && !mWindow->controls().shouldClose()) {
loop.processEvents(true);
}
runFrame();
}
finish();
}
void Application::addHangReportFileSource(HangWatchdog::ReportFileSource reportFileSource) {
if (mHangWatchdog) {
mHangWatchdog->addReportFileSource(reportFileSource);
}
mHangReportFileSources.push_back(std::move(reportFileSource));
}
void Application::enterPhase(UiPhase phase) {
if (mHangWatchdog) {
mHangWatchdog->enterPhase(phase);
}
}
bool Application::wantsFrame() { return mDelegate.wantsFrame() || mWindow->loop().hasPendingWork(); }
void Application::runFrame() {
updateGlobalTime();
const time_ms now = get_time();
const halnf deltaMs = halnf(now - mPreviousFrameTime);
mPreviousFrameTime = now;
runFrame(deltaMs);
}
void Application::runFrame(halnf deltaMs) {
mProfiler.beginFrame();
enterPhase(UiPhase::Input);
processPendingInput(deltaMs);
enterPhase(UiPhase::Draw);
present();
updateFrameCounters();
mProfiler.endFrame();
}
void Application::processPendingInput(halnf deltaMs) {
const ProfileScope scope(mProfiler, mProcessSection);
const size_t eventCount = mWindow->loop().advanceFrame();
mProfiler.addCounter(mInputEventsCounter, (double) eventCount);
if (mHangWatchdog) {
mHangWatchdog->noteInputEvents(eventCount);
}
mDelegate.processFrame(mWindow->input().state(), deltaMs);
mFramesProcessed++;
}
void Application::present() {
const ProfileScope scope(mProfiler, mPresentSection);
const std::optional<GpuFrame> frame = mWindow->loop().frameBegin();
if (!frame) {
return;
}
mCanvas->drawBegin(*frame, mWindow->frame());
mDebugGUI->drawBegin();
drawFrameContent();
enterPhase(UiPhase::Present);
mCanvas->drawEnd();
mDebugGUI->drawEnd(*frame);
mWindow->loop().frameEnd();
mFramesDrawn++;
}
void Application::drawFrameContent() {
const ProfileScope scope(mProfiler, mDrawSection);
const WindowFrame& frame = mWindow->frame();
mDecorations.paintBase(*mCanvas, frame);
{
const Canvas::SavedState saved(*mCanvas);
mDelegate.drawFrame(*mCanvas);
}
mDecorations.paintFrame(*mCanvas, frame, mWindow->controls().isFocused());
}
void Application::updateFrameCounters() {
if (!mPerSecondTimer.isTimeout()) {
return;
}
mPerSecondTimer.reset();
mFramesProcessedPerSecond = mFramesProcessed;
mFramesDrawnPerSecond = mFramesDrawn;
mFramesDrawn = 0;
mFramesProcessed = 0;
}
void Application::showFrameRates() {
mDebugGUI->showValue("Frames processed per second", mFramesProcessedPerSecond);
mDebugGUI->showValue("Frames drawn per second", mFramesDrawnPerSecond);
}

View file

@ -0,0 +1,175 @@
#include "SessionRecording.hpp"
#include <chrono>
#include <cstdio>
#include <ctime>
#include <cstdlib>
#include <filesystem>
#include <fstream>
namespace tp {
namespace {
double steadyMs() {
const auto now = std::chrono::steady_clock::now().time_since_epoch();
return std::chrono::duration<double, std::milli>(now).count();
}
bool writeText(const std::filesystem::path& path, const std::string& text) {
std::ofstream file(path, std::ios::binary | std::ios::trunc);
file.write(text.data(), (std::streamsize) text.size());
return (bool) file;
}
}
std::string SessionEnvironment::serialize() const {
char size[64];
std::snprintf(size, sizeof(size), "%.1f %.1f", windowSize.x, windowSize.y);
char scale[32];
std::snprintf(scale, sizeof(scale), "%.3f", contentScale);
return "application " + application + "\nrevision " + revision + "\ndevice " + device + "\ndate " + date +
"\nwindow " + size + "\nscale " + scale + "\n";
}
bool SessionRecording::save(const std::string& directory) const {
const std::filesystem::path root(directory);
std::error_code error;
std::filesystem::create_directories(root, error);
if (error) {
return false;
}
bool written = writeText(root / "environment.txt", environment.serialize());
written = written && input.save((root / "input.txt").string());
if (!recordedFrames.empty()) {
written = written && writeText(root / "recorded-frames.tsv", recordedFrames);
}
if (saveSnapshot) {
written = written && saveSnapshot(directory);
}
return written;
}
struct SessionRecorder::Recording {
explicit Recording(InputRecorder::Clock clock) :
input(std::move(clock)) {}
SessionEnvironment environment;
SessionRecording::Saver snapshot;
InputRecorder input;
double startMs = 0;
bool profilerWasEnabled = false;
};
SessionRecorder::SessionRecorder(Application& application, Clock clock) :
mApplication(application),
mClock(clock ? std::move(clock) : Clock(steadyMs)) {}
SessionRecorder::~SessionRecorder() { cancel(); }
double SessionRecorder::elapsedMs() const { return mRecording ? mClock() - mRecording->startMs : 0.0; }
SessionEnvironment SessionRecorder::environment() const {
SessionEnvironment environment;
environment.application = HangWatchdog::processName();
environment.revision = mRevision;
environment.device = mApplication.gpu().deviceName();
const std::time_t now = std::time(nullptr);
char date[64];
std::strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S", std::localtime(&now));
environment.date = date;
environment.windowSize = mApplication.window().contentSize();
environment.contentScale = mApplication.window().contentScale();
return environment;
}
void SessionRecorder::start() {
if (mRecording) {
return;
}
const SessionEnvironment recorded = environment();
mRecording = std::make_unique<Recording>(mClock);
mRecording->startMs = mClock();
mRecording->environment = recorded;
mRecording->snapshot = mSnapshot ? mSnapshot(recorded) : SessionRecording::Saver();
mRecording->input.setWindowSize(recorded.windowSize);
mRecording->input.startClockNow();
Profiler& profiler = mApplication.profiler();
mRecording->profilerWasEnabled = profiler.isEnabled();
profiler.clear();
profiler.setEnabled(true);
mApplication.window().input().setRawInputTap(&mRecording->input);
}
SessionRecording SessionRecorder::stop() {
SessionRecording session;
if (!mRecording) {
return session;
}
mApplication.window().input().setRawInputTap(nullptr);
session.environment = std::move(mRecording->environment);
session.input = mRecording->input.recording();
session.recordedFrames = recordedFramesTsv(mApplication.profiler());
session.saveSnapshot = std::move(mRecording->snapshot);
restoreProfiler();
return session;
}
void SessionRecorder::cancel() {
if (!mRecording) {
return;
}
mApplication.window().input().setRawInputTap(nullptr);
restoreProfiler();
}
void SessionRecorder::restoreProfiler() {
Profiler& profiler = mApplication.profiler();
profiler.setEnabled(mRecording->profilerWasEnabled);
profiler.clear();
mRecording.reset();
}
std::string SessionRecorder::defaultDirectory(const std::string& applicationSessionsDirectory) {
const char* configured = std::getenv("MODULES_SESSION_DIR");
if (configured && *configured) {
return configured;
}
return applicationSessionsDirectory;
}
std::string SessionRecorder::recordedFramesTsv(const Profiler& profiler) {
std::string text = "frame\twall_ms\tcpu_ms";
for (const std::string& name : profiler.sectionNames()) {
text += "\t" + name;
}
for (const std::string& name : profiler.counterNames()) {
text += "\t" + name;
}
text += "\n";
char value[32];
auto append = [&](double number) {
std::snprintf(value, sizeof(value), "\t%.4f", number);
text += value;
};
for (const FrameProfile& frame : profiler.frames()) {
text += std::to_string(frame.index);
append(frame.wallMs);
append(frame.cpuMs);
for (size_t index = 0; index < profiler.sectionNames().size(); index++) {
append(index < frame.sectionMs.size() ? frame.sectionMs[index] : 0.0);
}
for (size_t index = 0; index < profiler.counterNames().size(); index++) {
append(index < frame.counters.size() ? frame.counters[index] : 0.0);
}
text += "\n";
}
return text;
}
}

View file

@ -0,0 +1,97 @@
#pragma once
#include "Canvas.hpp"
#include "DebugGUI.hpp"
#include "HangWatchdog.hpp"
#include "Profiler.hpp"
#include "Timing.hpp"
#include "Window.hpp"
#include "WindowDecorations.hpp"
#include <filesystem>
#include <memory>
#include <vector>
namespace tp {
struct ApplicationOptions {
WindowOptions window = { .identity = WindowIdentity::generic() };
std::filesystem::path resourceDirectory = Window::platformResourceDirectory();
DebugInterface debugInterface = DebugInterface::Visible;
};
// What an application does each frame. Application drives the frame loop and calls it.
class ApplicationDelegate {
public:
virtual ~ApplicationDelegate() = default;
[[nodiscard]] virtual bool wantsFrame() { return false; }
virtual void processFrame(const InputState& input, halnf deltaMs) {}
virtual void drawFrame(Canvas& canvas) {}
};
class Application {
public:
explicit Application(ApplicationDelegate& delegate, const ApplicationOptions& options = {});
~Application();
Application(const Application&) = delete;
Application& operator=(const Application&) = delete;
void run();
void start();
void step(halnf deltaMs);
void finish();
[[nodiscard]] bool isRunning() const { return mRunning; }
[[nodiscard]] Window& window() { return *mWindow; }
[[nodiscard]] Canvas& canvas() { return *mCanvas; }
[[nodiscard]] DebugGUI& debugGUI() { return *mDebugGUI; }
[[nodiscard]] Profiler& profiler() { return mProfiler; }
[[nodiscard]] GpuDevice& gpu() { return *mGpu; }
void setHangSettings(HangSettings settings);
void addHangReportFileSource(HangWatchdog::ReportFileSource reportFileSource);
[[nodiscard]] HangWatchdog* hangWatchdog() { return mHangWatchdog.get(); }
[[nodiscard]] halnf framesProcessedPerSecond() const { return mFramesProcessedPerSecond; }
[[nodiscard]] halnf framesDrawnPerSecond() const { return mFramesDrawnPerSecond; }
void showFrameRates();
private:
void enterPhase(UiPhase phase);
[[nodiscard]] bool wantsFrame();
void runFrame();
void runFrame(halnf deltaMs);
void processPendingInput(halnf deltaMs);
void present();
void drawFrameContent();
void updateFrameCounters();
ApplicationDelegate& mDelegate;
Profiler mProfiler;
HangSettings mHangSettings;
bool mRunning = false;
time_ms mPreviousFrameTime = 0;
std::unique_ptr<HangWatchdog> mHangWatchdog;
std::vector<HangWatchdog::ReportFileSource> mHangReportFileSources;
Timer mPerSecondTimer;
halnf mFramesProcessedPerSecond = 0;
halnf mFramesDrawnPerSecond = 0;
halnf mFramesProcessed = 0;
halnf mFramesDrawn = 0;
ProfileId mProcessSection;
ProfileId mPresentSection;
ProfileId mDrawSection;
ProfileId mInputEventsCounter;
WindowDecorations mDecorations;
std::unique_ptr<GpuDevice> mGpu;
std::unique_ptr<Window> mWindow;
std::unique_ptr<Canvas> mCanvas;
std::unique_ptr<DebugGUI> mDebugGUI;
};
}

View file

@ -0,0 +1,73 @@
#pragma once
#include "GraphicApplication.hpp"
#include "InputRecording.hpp"
#include <functional>
#include <memory>
#include <string>
namespace tp {
struct SessionEnvironment {
std::string revision;
std::string device;
std::string date;
Vec2F windowSize = { 0, 0 };
halnf contentScale = 1;
std::string application;
[[nodiscard]] std::string serialize() const;
};
// A recorded session: the environment, every raw input event and the profiled frames, saved as one directory.
struct SessionRecording {
using Saver = std::function<bool(const std::string& directory)>;
SessionEnvironment environment;
InputRecording input;
std::string recordedFrames;
Saver saveSnapshot;
bool save(const std::string& directory) const;
};
// Records an application's raw input and frame profile between start() and stop(), for bug reports and replays.
class SessionRecorder {
public:
using Snapshot = std::function<SessionRecording::Saver(const SessionEnvironment& environment)>;
using Clock = std::function<double()>;
explicit SessionRecorder(Application& application, Clock clock = {});
~SessionRecorder();
SessionRecorder(const SessionRecorder&) = delete;
SessionRecorder& operator=(const SessionRecorder&) = delete;
void setSnapshot(Snapshot snapshot) { mSnapshot = std::move(snapshot); }
void setRevision(std::string revision) { mRevision = std::move(revision); }
[[nodiscard]] bool isRecording() const { return mRecording != nullptr; }
[[nodiscard]] double elapsedMs() const;
void start();
[[nodiscard]] SessionRecording stop();
void cancel();
[[nodiscard]] SessionEnvironment environment() const;
[[nodiscard]] static std::string defaultDirectory(const std::string& applicationSessionsDirectory);
[[nodiscard]] static std::string recordedFramesTsv(const Profiler& profiler);
private:
struct Recording;
void restoreProfiler();
Application& mApplication;
Clock mClock;
std::string mRevision = "dev";
Snapshot mSnapshot;
std::unique_ptr<Recording> mRecording;
};
}

View file

@ -0,0 +1,45 @@
#include "AndroidEntry.hpp"
#include "AndroidPlatform.hpp"
#include <android/log.h>
#include <android/native_activity.h>
#include <android_native_app_glue.h>
#include <cstdlib>
#include <exception>
namespace tp {
namespace {
void drainUntilDestroyed(android_app* app) {
while (!app->destroyRequested) {
int events = 0;
android_poll_source* source = nullptr;
if (ALooper_pollOnce(-1, nullptr, &events, (void**) &source) >= 0 && source) {
source->process(app, source);
}
}
}
}
void runAndroidApplication(
android_app* app, AndroidMain main, const char* const* assetDirectories, int assetDirectoryCount
) {
for (int index = 0; index < assetDirectoryCount; index++) {
android::extractAssets(*app, assetDirectories[index]);
}
setenv("HOME", app->activity->internalDataPath, 1);
try {
main(app);
} catch (const std::exception& error) {
__android_log_print(ANDROID_LOG_FATAL, "modules", "application failed: %s", error.what());
}
if (!app->destroyRequested) {
ANativeActivity_finish(app->activity);
}
drainUntilDestroyed(app);
}
}

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, Key>& keyCodes() {
static const std::unordered_map<int32_t, Key> keys = {
{ AKEYCODE_SPACE, Key::Space },
{ AKEYCODE_APOSTROPHE, Key::Apostrophe },
{ AKEYCODE_COMMA, Key::Comma },
{ AKEYCODE_MINUS, Key::Minus },
{ AKEYCODE_PERIOD, Key::Period },
{ AKEYCODE_SLASH, Key::Slash },
{ AKEYCODE_0, Key::Digit0 },
{ AKEYCODE_1, Key::Digit1 },
{ AKEYCODE_2, Key::Digit2 },
{ AKEYCODE_3, Key::Digit3 },
{ AKEYCODE_4, Key::Digit4 },
{ AKEYCODE_5, Key::Digit5 },
{ AKEYCODE_6, Key::Digit6 },
{ AKEYCODE_7, Key::Digit7 },
{ AKEYCODE_8, Key::Digit8 },
{ AKEYCODE_9, Key::Digit9 },
{ AKEYCODE_SEMICOLON, Key::Semicolon },
{ AKEYCODE_EQUALS, Key::Equal },
{ AKEYCODE_A, Key::A },
{ AKEYCODE_B, Key::B },
{ AKEYCODE_C, Key::C },
{ AKEYCODE_D, Key::D },
{ AKEYCODE_E, Key::E },
{ AKEYCODE_F, Key::F },
{ AKEYCODE_G, Key::G },
{ AKEYCODE_H, Key::H },
{ AKEYCODE_I, Key::I },
{ AKEYCODE_J, Key::J },
{ AKEYCODE_K, Key::K },
{ AKEYCODE_L, Key::L },
{ AKEYCODE_M, Key::M },
{ AKEYCODE_N, Key::N },
{ AKEYCODE_O, Key::O },
{ AKEYCODE_P, Key::P },
{ AKEYCODE_Q, Key::Q },
{ AKEYCODE_R, Key::R },
{ AKEYCODE_S, Key::S },
{ AKEYCODE_T, Key::T },
{ AKEYCODE_U, Key::U },
{ AKEYCODE_V, Key::V },
{ AKEYCODE_W, Key::W },
{ AKEYCODE_X, Key::X },
{ AKEYCODE_Y, Key::Y },
{ AKEYCODE_Z, Key::Z },
{ AKEYCODE_LEFT_BRACKET, Key::LeftBracket },
{ AKEYCODE_BACKSLASH, Key::Backslash },
{ AKEYCODE_RIGHT_BRACKET, Key::RightBracket },
{ AKEYCODE_GRAVE, Key::GraveAccent },
{ AKEYCODE_ESCAPE, Key::Escape },
{ AKEYCODE_ENTER, Key::Enter },
{ AKEYCODE_TAB, Key::Tab },
{ AKEYCODE_DEL, Key::Backspace },
{ AKEYCODE_INSERT, Key::Insert },
{ AKEYCODE_FORWARD_DEL, Key::Delete },
{ AKEYCODE_DPAD_RIGHT, Key::Right },
{ AKEYCODE_DPAD_LEFT, Key::Left },
{ AKEYCODE_DPAD_DOWN, Key::Down },
{ AKEYCODE_DPAD_UP, Key::Up },
{ AKEYCODE_PAGE_UP, Key::PageUp },
{ AKEYCODE_PAGE_DOWN, Key::PageDown },
{ AKEYCODE_MOVE_HOME, Key::Home },
{ AKEYCODE_MOVE_END, Key::End },
{ AKEYCODE_CAPS_LOCK, Key::CapsLock },
{ AKEYCODE_SCROLL_LOCK, Key::ScrollLock },
{ AKEYCODE_NUM_LOCK, Key::NumLock },
{ AKEYCODE_SYSRQ, Key::PrintScreen },
{ AKEYCODE_BREAK, Key::Pause },
{ AKEYCODE_F1, Key::F1 },
{ AKEYCODE_F2, Key::F2 },
{ AKEYCODE_F3, Key::F3 },
{ AKEYCODE_F4, Key::F4 },
{ AKEYCODE_F5, Key::F5 },
{ AKEYCODE_F6, Key::F6 },
{ AKEYCODE_F7, Key::F7 },
{ AKEYCODE_F8, Key::F8 },
{ AKEYCODE_F9, Key::F9 },
{ AKEYCODE_F10, Key::F10 },
{ AKEYCODE_F11, Key::F11 },
{ AKEYCODE_F12, Key::F12 },
{ AKEYCODE_NUMPAD_0, Key::Keypad0 },
{ AKEYCODE_NUMPAD_1, Key::Keypad1 },
{ AKEYCODE_NUMPAD_2, Key::Keypad2 },
{ AKEYCODE_NUMPAD_3, Key::Keypad3 },
{ AKEYCODE_NUMPAD_4, Key::Keypad4 },
{ AKEYCODE_NUMPAD_5, Key::Keypad5 },
{ AKEYCODE_NUMPAD_6, Key::Keypad6 },
{ AKEYCODE_NUMPAD_7, Key::Keypad7 },
{ AKEYCODE_NUMPAD_8, Key::Keypad8 },
{ AKEYCODE_NUMPAD_9, Key::Keypad9 },
{ AKEYCODE_NUMPAD_DOT, Key::KeypadDecimal },
{ AKEYCODE_NUMPAD_DIVIDE, Key::KeypadDivide },
{ AKEYCODE_NUMPAD_MULTIPLY, Key::KeypadMultiply },
{ AKEYCODE_NUMPAD_SUBTRACT, Key::KeypadSubtract },
{ AKEYCODE_NUMPAD_ADD, Key::KeypadAdd },
{ AKEYCODE_NUMPAD_ENTER, Key::KeypadEnter },
{ AKEYCODE_NUMPAD_EQUALS, Key::KeypadEqual },
{ AKEYCODE_SHIFT_LEFT, Key::LeftShift },
{ AKEYCODE_CTRL_LEFT, Key::LeftControl },
{ AKEYCODE_ALT_LEFT, Key::LeftAlt },
{ AKEYCODE_META_LEFT, Key::LeftSuper },
{ AKEYCODE_SHIFT_RIGHT, Key::RightShift },
{ AKEYCODE_CTRL_RIGHT, Key::RightControl },
{ AKEYCODE_ALT_RIGHT, Key::RightAlt },
{ AKEYCODE_META_RIGHT, Key::RightSuper },
{ AKEYCODE_MENU, Key::Menu },
};
return keys;
}
}
std::optional<Key> keyFromAndroid(int32_t keyCode) {
const auto& keys = keyCodes();
const auto found = keys.find(keyCode);
if (found == keys.end()) {
return std::nullopt;
}
return found->second;
}
}

View file

@ -0,0 +1,11 @@
#pragma once
#include "InputCodes.hpp"
#include <cstdint>
#include <optional>
namespace tp::android {
[[nodiscard]] std::optional<Key> keyFromAndroid(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,505 @@
#include "AndroidWindow.hpp"
#include "AndroidKeys.hpp"
#include "AndroidPlatform.hpp"
#include <android/configuration.h>
#include <sys/system_properties.h>
#include <android/input.h>
#include <android/log.h>
#include <android/native_activity.h>
#include <android/native_window.h>
#include <android_native_app_glue.h>
#include <vulkan/vulkan_android.h>
#include <algorithm>
#include <filesystem>
#include <cmath>
#include <cstdlib>
#include <optional>
#include <stdexcept>
namespace tp {
namespace {
constexpr const char* kTag = "modules";
constexpr halnf kReferenceDensity = 160.0f;
constexpr halnf kWheelStep = 1.0f;
PointerDevice deviceFor(int32_t type) {
switch (type) {
case AMOTION_EVENT_TOOL_TYPE_STYLUS:
case AMOTION_EVENT_TOOL_TYPE_ERASER: return PointerDevice::Stylus;
case AMOTION_EVENT_TOOL_TYPE_FINGER: return PointerDevice::Finger;
default: return PointerDevice::Mouse;
}
}
StylusTip tipFor(int32_t type) {
return type == AMOTION_EVENT_TOOL_TYPE_ERASER ? StylusTip::Eraser : StylusTip::Pen;
}
std::optional<PointerButton> buttonFor(int32_t androidButton) {
switch (androidButton) {
case AMOTION_EVENT_BUTTON_PRIMARY: return PointerButton::Left;
case AMOTION_EVENT_BUTTON_SECONDARY: return PointerButton::Right;
case AMOTION_EVENT_BUTTON_TERTIARY: return PointerButton::Middle;
case AMOTION_EVENT_BUTTON_BACK: return PointerButton::Back;
case AMOTION_EVENT_BUTTON_FORWARD: return PointerButton::Forward;
default: return std::nullopt;
}
}
}
AndroidWindow::AndroidWindow(PlatformWindowHost& host, android_app& app) :
mHost(host),
mApp(app) {
mFrame.setTranslucent(false);
mFrame.setTiled(true);
mApp.userData = this;
mApp.onAppCmd = onCommand;
mApp.onInputEvent = onInput;
if (mApp.config) {
const int32_t density = AConfiguration_getDensity(mApp.config);
if (density > 0 && density != ACONFIGURATION_DENSITY_ANY) {
mScale = (halnf) density / kReferenceDensity;
}
}
while (!mApp.window && !mApp.destroyRequested) {
pollOnce(-1);
}
if (mApp.window) {
attachWindow(mApp.window);
}
}
AndroidWindow::~AndroidWindow() {
mApp.onAppCmd = nullptr;
mApp.onInputEvent = nullptr;
mApp.userData = nullptr;
}
VkSurfaceKHR AndroidWindow::createSurface(VkInstance instance) const {
if (!mNativeWindow) {
return VK_NULL_HANDLE;
}
VkAndroidSurfaceCreateInfoKHR info{
.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR,
.window = mNativeWindow,
};
VkSurfaceKHR surface = VK_NULL_HANDLE;
if (vkCreateAndroidSurfaceKHR(instance, &info, nullptr, &surface) != VK_SUCCESS) {
return VK_NULL_HANDLE;
}
return surface;
}
VkExtent2D AndroidWindow::framebufferExtent() const {
if (!mNativeWindow) {
return { 0, 0 };
}
return { (uint32_t) ANativeWindow_getWidth(mNativeWindow), (uint32_t) ANativeWindow_getHeight(mNativeWindow) };
}
void AndroidWindow::onTranslucencyResolved(bool) { mFrame.setTranslucent(false); }
void AndroidWindow::updateGeometry() {
if (!mNativeWindow) {
return;
}
const halnf width = (halnf) ANativeWindow_getWidth(mNativeWindow) / mScale;
const halnf height = (halnf) ANativeWindow_getHeight(mNativeWindow) / mScale;
if (width <= 0 || height <= 0) {
return;
}
const EdgeInsets pixels = android::systemInsets(mApp);
const EdgeInsets insets = {
pixels.left / mScale, pixels.top / mScale, pixels.right / mScale, pixels.bottom / mScale
};
const bool sizeChanged = Vec2F(width, height) != mFrame.contentSize();
if (!sizeChanged && insets == mSafeInsets) {
return;
}
mSafeInsets = insets;
mFrame.setContentSize({ width, height });
if (mConfigured) {
mHost.onContentResized();
}
}
void AndroidWindow::attachWindow(ANativeWindow* window) {
mNativeWindow = window;
updateGeometry();
mHost.onSurfaceAvailable();
__android_log_print(
ANDROID_LOG_INFO,
kTag,
"window %dx%d scale %.2f",
ANativeWindow_getWidth(window),
ANativeWindow_getHeight(window),
mScale
);
}
void AndroidWindow::detachWindow() {
mHost.onSurfaceLost();
mNativeWindow = nullptr;
}
void AndroidWindow::onCommand(android_app* app, int32_t command) {
if (auto* self = static_cast<AndroidWindow*>(app->userData)) {
self->handleCommand(command);
}
}
int32_t AndroidWindow::onInput(android_app* app, AInputEvent* event) {
auto* self = static_cast<AndroidWindow*>(app->userData);
return self && self->handleInput(event) ? 1 : 0;
}
void AndroidWindow::handleCommand(int32_t command) {
switch (command) {
case APP_CMD_INIT_WINDOW:
if (mApp.window) {
attachWindow(mApp.window);
}
break;
case APP_CMD_TERM_WINDOW: detachWindow(); break;
case APP_CMD_WINDOW_RESIZED:
case APP_CMD_CONFIG_CHANGED:
case APP_CMD_CONTENT_RECT_CHANGED: updateGeometry(); break;
case APP_CMD_GAINED_FOCUS:
mFocused = true;
input().post(KeyboardFocusChanged{ true });
break;
case APP_CMD_LOST_FOCUS:
mFocused = false;
input().post(KeyboardFocusChanged{ false });
break;
default: break;
}
}
bool AndroidWindow::handleInput(AInputEvent* event) {
switch (AInputEvent_getType(event)) {
case AINPUT_EVENT_TYPE_MOTION: return handleMotion(event);
case AINPUT_EVENT_TYPE_KEY: return handleKey(event);
default: return false;
}
}
bool AndroidWindow::handleMotion(AInputEvent* event) {
const int32_t action = AMotionEvent_getAction(event) & AMOTION_EVENT_ACTION_MASK;
const int32_t toolType = AMotionEvent_getToolType(event, 0);
if (toolType == AMOTION_EVENT_TOOL_TYPE_STYLUS || toolType == AMOTION_EVENT_TOOL_TYPE_ERASER) {
mPenSeen = true;
}
if (toolType == AMOTION_EVENT_TOOL_TYPE_MOUSE) {
mMouseSeen = true;
}
InputSink& input = this->input();
if (toolType == AMOTION_EVENT_TOOL_TYPE_FINGER && handleTouchGesture(event, action)) {
return true;
}
const auto position = [&](size_t history) -> Vec2F {
if (history == SIZE_MAX) {
return { AMotionEvent_getX(event, 0) / mScale, AMotionEvent_getY(event, 0) / mScale };
}
return { AMotionEvent_getHistoricalX(event, 0, history) / mScale,
AMotionEvent_getHistoricalY(event, 0, history) / mScale };
};
const auto sample = [&](size_t history) {
PointerSample result;
result.device = deviceFor(toolType);
result.tip = tipFor(toolType);
const bool current = history == SIZE_MAX;
result.pressure =
current ? AMotionEvent_getPressure(event, 0) : AMotionEvent_getHistoricalPressure(event, 0, history);
const halnf tilt = current ? AMotionEvent_getAxisValue(event, AMOTION_EVENT_AXIS_TILT, 0)
: AMotionEvent_getHistoricalAxisValue(event, AMOTION_EVENT_AXIS_TILT, 0, history);
const halnf orientation =
current ? AMotionEvent_getOrientation(event, 0) : AMotionEvent_getHistoricalOrientation(event, 0, history);
result.tilt = { std::sin(orientation) * tilt, -std::cos(orientation) * tilt };
result.rotation = orientation;
result.timeMs =
(uint32_t) ((current ? AMotionEvent_getEventTime(event) : AMotionEvent_getHistoricalEventTime(event, history)) /
1000000);
if (!result.isStylus()) {
result.pressure = 1;
} else if (result.pressure > 0) {
mLastContactPressure = result.pressure;
}
return result;
};
switch (action) {
case AMOTION_EVENT_ACTION_DOWN:
case AMOTION_EVENT_ACTION_BUTTON_PRESS:
{
input.post(PointerSampled{ sample(SIZE_MAX) });
input.post(PointerMoved{ position(SIZE_MAX) });
if (action == AMOTION_EVENT_ACTION_BUTTON_PRESS) {
deliverButtonChanges(input, AMotionEvent_getButtonState(event));
} else if (
!mPrimaryHeld && (AMotionEvent_getButtonState(event) == 0 || toolType != AMOTION_EVENT_TOOL_TYPE_MOUSE)
) {
mPrimaryHeld = true;
input.post(PointerButtonChanged{ PointerButton::Left, true });
}
return true;
}
case AMOTION_EVENT_ACTION_MOVE:
case AMOTION_EVENT_ACTION_HOVER_MOVE:
{
const size_t history = AMotionEvent_getHistorySize(event);
for (size_t index = 0; index < history; index++) {
input.post(PointerSampled{ sample(index) });
input.post(PointerMoved{ position(index) });
}
input.post(PointerSampled{ sample(SIZE_MAX) });
input.post(PointerMoved{ position(SIZE_MAX) });
return true;
}
case AMOTION_EVENT_ACTION_UP:
case AMOTION_EVENT_ACTION_CANCEL:
case AMOTION_EVENT_ACTION_BUTTON_RELEASE:
{
PointerSample released = sample(SIZE_MAX);
if (released.isStylus()) {
released.pressure = mLastContactPressure;
}
input.post(PointerSampled{ released });
input.post(PointerMoved{ position(SIZE_MAX) });
if (action == AMOTION_EVENT_ACTION_BUTTON_RELEASE) {
deliverButtonChanges(input, AMotionEvent_getButtonState(event));
} else if (mPrimaryHeld) {
mPrimaryHeld = false;
input.post(PointerButtonChanged{ PointerButton::Left, false });
}
if (toolType == AMOTION_EVENT_TOOL_TYPE_FINGER) {
input.post(PointerLeft{});
}
return true;
}
case AMOTION_EVENT_ACTION_HOVER_EXIT: input.post(PointerLeft{}); return true;
case AMOTION_EVENT_ACTION_SCROLL:
{
const Vec2F delta = { -AMotionEvent_getAxisValue(event, AMOTION_EVENT_AXIS_HSCROLL, 0) * kWheelStep,
AMotionEvent_getAxisValue(event, AMOTION_EVENT_AXIS_VSCROLL, 0) * kWheelStep };
input.post(PointerMoved{ position(SIZE_MAX) });
input.post(Scrolled{ delta, ScrollSource::Wheel });
return true;
}
default: return false;
}
}
bool AndroidWindow::handleTouchGesture(AInputEvent* event, int32_t action) {
const size_t count = AMotionEvent_getPointerCount(event);
const bool lifting = action == AMOTION_EVENT_ACTION_POINTER_UP;
const size_t liftedIndex =
lifting ? (size_t) ((AMotionEvent_getAction(event) & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >>
AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT)
: SIZE_MAX;
const bool ending = action == AMOTION_EVENT_ACTION_UP || action == AMOTION_EVENT_ACTION_CANCEL;
std::vector<TouchPoint> touches;
if (!ending) {
for (size_t index = 0; index < count; index++) {
if (index == liftedIndex) {
continue;
}
touches.push_back(
{ AMotionEvent_getPointerId(event, index),
{ AMotionEvent_getX(event, index) / mScale, AMotionEvent_getY(event, index) / mScale } }
);
}
}
if (touches.size() < 2 && !mGestures.isActive() && !mGestureHoldsPointer) {
return false;
}
InputSink& input = this->input();
std::vector<TouchGesture> gestures;
mGestures.update(touches, gestures, (uint32_t) (AMotionEvent_getEventTime(event) / 1000000));
for (const TouchGesture& gesture : gestures) {
input.post(GestureChanged{ gesture });
}
if (touches.size() >= 2 && !mGestureHoldsPointer) {
mGestureHoldsPointer = true;
if (mPrimaryHeld) {
mPrimaryHeld = false;
input.post(PointerButtonChanged{ PointerButton::Left, false });
}
input.post(PointerLeft{});
}
if (ending) {
mGestureHoldsPointer = false;
}
return true;
}
void AndroidWindow::deliverButtonChanges(InputSink& input, int32_t state) {
const int32_t changed = state ^ mButtonState;
for (int32_t bit = 1; bit <= AMOTION_EVENT_BUTTON_FORWARD; bit <<= 1) {
if (!(changed & bit)) {
continue;
}
if (auto button = buttonFor(bit)) {
input.post(PointerButtonChanged{ *button, (state & bit) != 0 });
}
}
mButtonState = state;
}
bool AndroidWindow::handleKey(AInputEvent* event) {
const int32_t keyCode = AKeyEvent_getKeyCode(event);
if (keyCode == AKEYCODE_VOLUME_UP || keyCode == AKEYCODE_VOLUME_DOWN) {
return false;
}
const int32_t action = AKeyEvent_getAction(event);
InputSink& input = this->input();
const std::optional<Key> key =
keyCode == AKEYCODE_BACK ? std::optional(Key::Escape) : android::keyFromAndroid(keyCode);
if (key && action == AKEY_EVENT_ACTION_DOWN) {
const bool repeats = AKeyEvent_getRepeatCount(event) > 0;
input.post(KeyChanged{ *key, repeats ? KeyAction::Repeat : KeyAction::Press });
} else if (key && action == AKEY_EVENT_ACTION_UP) {
input.post(KeyChanged{ *key, KeyAction::Release });
}
if (action == AKEY_EVENT_ACTION_DOWN) {
const std::string text = android::unicodeText(mApp, event);
if (!text.empty()) {
input.post(TextEntered{ text });
}
}
return true;
}
void AndroidWindow::pollOnce(int timeoutMs) {
int events = 0;
android_poll_source* source = nullptr;
if (ALooper_pollOnce(timeoutMs, nullptr, &events, (void**) &source) >= 0 && source) {
source->process(&mApp, source);
}
}
void AndroidWindow::wake() {
if (mApp.looper) {
ALooper_wake(mApp.looper);
}
}
void AndroidWindow::dispatchEvents(halnf timeoutMs) {
int timeout = timeoutMs < 0 ? -1 : (int) std::ceil(timeoutMs);
if (!mNativeWindow && timeout >= 0) {
timeout = 100;
}
pollOnce(timeout);
while (true) {
int events = 0;
android_poll_source* source = nullptr;
if (ALooper_pollOnce(0, nullptr, &events, (void**) &source) < 0) {
break;
}
if (source) {
source->process(&mApp, source);
}
if (mApp.destroyRequested) {
break;
}
}
}
void AndroidWindow::waitUntilConfigured() {
while (!mNativeWindow && !mApp.destroyRequested) {
pollOnce(-1);
}
updateGeometry();
mConfigured = true;
mHost.onContentResized();
__android_log_print(ANDROID_LOG_INFO, kTag, "platform %s", describe(platformInfo()).c_str());
}
bool AndroidWindow::shouldClose() const { return mApp.destroyRequested != 0; }
void AndroidWindow::requestClose() { ANativeActivity_finish(mApp.activity); }
void AndroidWindow::minimize() { ANativeActivity_finish(mApp.activity); }
bool AndroidWindow::openUrl(const std::string& url) { return android::openUrl(mApp, url); }
void AndroidWindow::setTextInputEnabled(bool enabled) {
if (enabled == mTextInput) {
return;
}
mTextInput = enabled;
android::setSoftKeyboardVisible(mApp, enabled);
}
namespace {
std::string systemProperty(const char* name) {
char value[PROP_VALUE_MAX] = {};
__system_property_get(name, value);
return value;
}
constexpr int32_t kTabletSmallestWidthDp = 600;
}
PlatformInfo AndroidWindow::platformInfo() const {
PlatformInfo info;
info.system = OperatingSystem::Android;
info.systemVersion = systemProperty("ro.build.version.release");
info.deviceName = systemProperty("ro.product.manufacturer") + " " + systemProperty("ro.product.model");
info.contentScale = mScale;
info.screenSize = mFrame.contentSize();
info.formFactor = FormFactor::Phone;
if (AConfiguration* config = mApp.config) {
const int32_t smallest = AConfiguration_getSmallestScreenWidthDp(config);
if (smallest >= kTabletSmallestWidthDp) {
info.formFactor = FormFactor::Tablet;
}
if (AConfiguration_getUiModeType(config) == ACONFIGURATION_UI_MODE_TYPE_DESK) {
info.formFactor = FormFactor::Desktop;
}
info.hasTouch = AConfiguration_getTouchscreen(config) == ACONFIGURATION_TOUCHSCREEN_FINGER;
info.hasPhysicalKeyboard = AConfiguration_getKeyboard(config) == ACONFIGURATION_KEYBOARD_QWERTY;
}
info.safeInsets = mSafeInsets;
info.hasPen = mPenSeen;
info.hasMouse = mMouseSeen;
info.hasHoverPointer = mMouseSeen || mPenSeen;
return info;
}
std::vector<const char*> platformSurfaceExtensions() {
return { VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME };
}
// runAndroidApplication extracts the packaged assets into the app's internal storage, which it exports as HOME.
std::filesystem::path platformResourceDirectory() {
const char* home = std::getenv("HOME");
return home ? std::filesystem::path(home) / "rsc" : std::filesystem::path("rsc");
}
std::unique_ptr<PlatformWindow>
createPlatformWindow(PlatformWindowHost& host, Vec2F, const WindowIdentity&, const PlatformContext& context) {
if (!context.androidApp) {
throw std::runtime_error("the Android backend needs PlatformContext::androidApp");
}
return std::make_unique<AndroidWindow>(host, *context.androidApp);
}
}

View file

@ -0,0 +1,91 @@
#pragma once
#include "PlatformWindow.hpp"
#include "TouchGesture.hpp"
struct android_app;
struct ANativeWindow;
struct AInputEvent;
namespace tp {
class AndroidWindow final : public PlatformWindow {
public:
AndroidWindow(PlatformWindowHost& host, android_app& app);
~AndroidWindow() override;
AndroidWindow(const AndroidWindow&) = delete;
AndroidWindow& operator=(const AndroidWindow&) = delete;
[[nodiscard]] VkSurfaceKHR createSurface(VkInstance instance) const override;
[[nodiscard]] VkExtent2D framebufferExtent() const override;
[[nodiscard]] bool wantsTranslucency() const override { return false; }
void onTranslucencyResolved(bool translucent) override;
void setAcceptsInput(bool) override {}
void dispatchEvents(halnf timeoutMs) override;
void wake() override;
void waitUntilConfigured() override;
[[nodiscard]] bool hasPendingConfigure() const override { return false; }
void applyPendingConfigure() override {}
void commitFrameGeometry() override {}
[[nodiscard]] bool isWaitingForFrame() const override { return mNativeWindow == nullptr; }
[[nodiscard]] bool shouldClose() const override;
void requestClose() override;
[[nodiscard]] const WindowFrame& frame() const override { return mFrame; }
[[nodiscard]] halnf scale() const override { return mScale; }
[[nodiscard]] bool isMaximized() const override { return true; }
[[nodiscard]] bool isFocused() const override { return mFocused; }
void minimize() override;
[[nodiscard]] bool isFullScreen() const override { return true; }
void setCursor(CursorShape) override {}
void setClipboardText(const std::string& text) override { mClipboard = text; }
[[nodiscard]] std::string clipboardText() override { return mClipboard; }
[[nodiscard]] ClipboardImage clipboardImage() override { return {}; }
bool openUrl(const std::string& url) override;
void setTextInputEnabled(bool enabled) override;
void setTextInputRect(const RectF&) override {}
[[nodiscard]] PlatformInfo platformInfo() const override;
private:
static void onCommand(android_app* app, int32_t command);
static int32_t onInput(android_app* app, AInputEvent* event);
void handleCommand(int32_t command);
[[nodiscard]] bool handleInput(AInputEvent* event);
[[nodiscard]] bool handleMotion(AInputEvent* event);
[[nodiscard]] bool handleKey(AInputEvent* event);
void deliverButtonChanges(InputSink& input, int32_t state);
[[nodiscard]] bool handleTouchGesture(AInputEvent* event, int32_t action);
void attachWindow(ANativeWindow* window);
void detachWindow();
void updateGeometry();
void pollOnce(int timeoutMs);
[[nodiscard]] InputSink& input() { return mHost.rawInput(); }
PlatformWindowHost& mHost;
android_app& mApp;
ANativeWindow* mNativeWindow = nullptr;
WindowFrame mFrame;
std::string mClipboard;
halnf mScale = 1;
bool mFocused = false;
bool mConfigured = false;
bool mPrimaryHeld = false;
bool mTextInput = false;
EdgeInsets mSafeInsets;
bool mPenSeen = false;
bool mMouseSeen = false;
int32_t mButtonState = 0;
halnf mLastContactPressure = 1;
TouchGestureTracker mGestures;
bool mGestureHoldsPointer = false;
};
}

View file

@ -0,0 +1,13 @@
#pragma once
struct android_app;
namespace tp {
using AndroidMain = int (*)(android_app*);
// Extracts assets, runs main and finishes the activity when main returns or fails.
void runAndroidApplication(
android_app* app, AndroidMain main, const char* const* assetDirectories, int assetDirectoryCount
);
}

View file

@ -0,0 +1,50 @@
#include "PlatformInfo.hpp"
#include <cstdio>
namespace tp {
const char* toString(OperatingSystem system) {
switch (system) {
case OperatingSystem::Linux: return "Linux";
case OperatingSystem::Android: return "Android";
case OperatingSystem::Windows: return "Windows";
case OperatingSystem::MacOS: return "macOS";
case OperatingSystem::IOS: return "iOS";
case OperatingSystem::Unknown: break;
}
return "unknown";
}
const char* toString(FormFactor formFactor) {
switch (formFactor) {
case FormFactor::Desktop: return "desktop";
case FormFactor::Tablet: return "tablet";
case FormFactor::Phone: return "phone";
case FormFactor::Unknown: break;
}
return "unknown";
}
std::string describe(const PlatformInfo& info) {
char text[512];
std::snprintf(
text,
sizeof(text),
"%s %s, %s%s%s, scale %.2f, screen %.0fx%.0f, input:%s%s%s%s",
toString(info.system),
info.systemVersion.c_str(),
toString(info.formFactor),
info.deviceName.empty() ? "" : " ",
info.deviceName.c_str(),
info.contentScale,
info.screenSize.x,
info.screenSize.y,
info.hasMouse ? " mouse" : "",
info.hasTouch ? " touch" : "",
info.hasPen ? " pen" : "",
info.hasPhysicalKeyboard ? " keyboard" : ""
);
return text;
}
}

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,19 @@
#pragma once
#include "Vec.hpp"
#include "WindowTypes.hpp"
namespace tp {
// Window manipulation that only desktop window systems offer; absent on other platforms.
class DesktopWindowControls {
public:
virtual ~DesktopWindowControls() = default;
virtual void toggleMaximize() = 0;
virtual void setFullScreen(bool fullScreen) = 0;
virtual void beginMove() = 0;
virtual void beginResize(WindowEdge edge) = 0;
virtual void showWindowMenu(Vec2F contentPosition) = 0;
};
}

View file

@ -0,0 +1,11 @@
#pragma once
struct android_app;
namespace tp {
// Process objects a backend needs to open a window; desktop backends need none.
struct PlatformContext {
android_app* androidApp = nullptr;
};
}

View file

@ -0,0 +1,47 @@
#pragma once
#include "Vec.hpp"
#include <string>
namespace tp {
enum class OperatingSystem { Linux, Android, Windows, MacOS, IOS, Unknown };
enum class FormFactor { Desktop, Tablet, Phone, Unknown };
struct EdgeInsets {
halnf left = 0;
halnf top = 0;
halnf right = 0;
halnf bottom = 0;
[[nodiscard]] bool operator==(const EdgeInsets& other) const = default;
};
struct PlatformInfo {
OperatingSystem system = OperatingSystem::Unknown;
FormFactor formFactor = FormFactor::Unknown;
std::string systemVersion;
std::string deviceName;
halnf contentScale = 1;
Vec2F screenSize = { 0, 0 };
EdgeInsets safeInsets;
bool hasMouse = false;
bool hasTouch = false;
bool hasPen = false;
bool hasPhysicalKeyboard = false;
bool hasHoverPointer = false;
[[nodiscard]] bool isTouchFirst() const {
return formFactor == FormFactor::Tablet || formFactor == FormFactor::Phone;
}
};
[[nodiscard]] const char* toString(OperatingSystem system);
[[nodiscard]] const char* toString(FormFactor formFactor);
[[nodiscard]] std::string describe(const PlatformInfo& info);
}

View file

@ -0,0 +1,73 @@
#pragma once
#include "InputSink.hpp"
#include "PlatformContext.hpp"
#include "PlatformInfo.hpp"
#include "Rect.hpp"
#include "WindowFrame.hpp"
#include "WindowSurface.hpp"
#include "WindowTypes.hpp"
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
namespace tp {
class DesktopWindowControls;
// What a backend reports to the window that owns it.
class PlatformWindowHost {
public:
virtual ~PlatformWindowHost() = default;
[[nodiscard]] virtual InputSink& rawInput() = 0;
virtual void onContentResized() = 0;
virtual void onSurfaceAvailable() = 0;
virtual void onSurfaceLost() = 0;
};
class PlatformWindow : public VulkanSurfaceSource {
public:
~PlatformWindow() override = default;
virtual void dispatchEvents(halnf timeoutMs) = 0;
virtual void wake() = 0;
virtual void waitUntilConfigured() = 0;
[[nodiscard]] virtual bool hasPendingConfigure() const = 0;
virtual void applyPendingConfigure() = 0;
virtual void commitFrameGeometry() = 0;
[[nodiscard]] virtual bool isWaitingForFrame() const = 0;
virtual void setAcceptsInput(bool accepts) = 0;
[[nodiscard]] virtual bool shouldClose() const = 0;
virtual void requestClose() = 0;
[[nodiscard]] virtual const WindowFrame& frame() const = 0;
[[nodiscard]] virtual halnf scale() const = 0;
[[nodiscard]] virtual bool isMaximized() const = 0;
[[nodiscard]] virtual bool isFullScreen() const = 0;
[[nodiscard]] virtual bool isFocused() const = 0;
virtual void minimize() = 0;
[[nodiscard]] virtual DesktopWindowControls* desktopControls() { return nullptr; }
virtual void setCursor(CursorShape shape) = 0;
virtual void setClipboardText(const std::string& text) = 0;
[[nodiscard]] virtual std::string clipboardText() = 0;
[[nodiscard]] virtual ClipboardImage clipboardImage() = 0;
virtual bool openUrl(const std::string& url) = 0;
virtual void setTextInputEnabled(bool enabled) = 0;
virtual void setTextInputRect(const RectF& contentRect) = 0;
[[nodiscard]] virtual PlatformInfo platformInfo() const = 0;
};
[[nodiscard]] std::vector<const char*> platformSurfaceExtensions();
[[nodiscard]] std::filesystem::path platformResourceDirectory();
[[nodiscard]] std::unique_ptr<PlatformWindow> createPlatformWindow(
PlatformWindowHost& host, Vec2F contentSize, const WindowIdentity& identity, const PlatformContext& context
);
}

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,153 @@
#include "LinuxInputCodes.hpp"
#include <linux/input-event-codes.h>
#include <unordered_map>
using namespace tp;
namespace {
const std::unordered_map<uint32_t, Key>& evdevKeys() {
static const std::unordered_map<uint32_t, Key> keys = {
{ KEY_SPACE, Key::Space },
{ KEY_APOSTROPHE, Key::Apostrophe },
{ KEY_COMMA, Key::Comma },
{ KEY_MINUS, Key::Minus },
{ KEY_DOT, Key::Period },
{ KEY_SLASH, Key::Slash },
{ KEY_0, Key::Digit0 },
{ KEY_1, Key::Digit1 },
{ KEY_2, Key::Digit2 },
{ KEY_3, Key::Digit3 },
{ KEY_4, Key::Digit4 },
{ KEY_5, Key::Digit5 },
{ KEY_6, Key::Digit6 },
{ KEY_7, Key::Digit7 },
{ KEY_8, Key::Digit8 },
{ KEY_9, Key::Digit9 },
{ KEY_SEMICOLON, Key::Semicolon },
{ KEY_EQUAL, Key::Equal },
{ KEY_A, Key::A },
{ KEY_B, Key::B },
{ KEY_C, Key::C },
{ KEY_D, Key::D },
{ KEY_E, Key::E },
{ KEY_F, Key::F },
{ KEY_G, Key::G },
{ KEY_H, Key::H },
{ KEY_I, Key::I },
{ KEY_J, Key::J },
{ KEY_K, Key::K },
{ KEY_L, Key::L },
{ KEY_M, Key::M },
{ KEY_N, Key::N },
{ KEY_O, Key::O },
{ KEY_P, Key::P },
{ KEY_Q, Key::Q },
{ KEY_R, Key::R },
{ KEY_S, Key::S },
{ KEY_T, Key::T },
{ KEY_U, Key::U },
{ KEY_V, Key::V },
{ KEY_W, Key::W },
{ KEY_X, Key::X },
{ KEY_Y, Key::Y },
{ KEY_Z, Key::Z },
{ KEY_LEFTBRACE, Key::LeftBracket },
{ KEY_BACKSLASH, Key::Backslash },
{ KEY_RIGHTBRACE, Key::RightBracket },
{ KEY_GRAVE, Key::GraveAccent },
{ KEY_102ND, Key::World2 },
{ KEY_ESC, Key::Escape },
{ KEY_ENTER, Key::Enter },
{ KEY_TAB, Key::Tab },
{ KEY_BACKSPACE, Key::Backspace },
{ KEY_INSERT, Key::Insert },
{ KEY_DELETE, Key::Delete },
{ KEY_RIGHT, Key::Right },
{ KEY_LEFT, Key::Left },
{ KEY_DOWN, Key::Down },
{ KEY_UP, Key::Up },
{ KEY_PAGEUP, Key::PageUp },
{ KEY_PAGEDOWN, Key::PageDown },
{ KEY_HOME, Key::Home },
{ KEY_END, Key::End },
{ KEY_CAPSLOCK, Key::CapsLock },
{ KEY_SCROLLLOCK, Key::ScrollLock },
{ KEY_NUMLOCK, Key::NumLock },
{ KEY_SYSRQ, Key::PrintScreen },
{ KEY_PAUSE, Key::Pause },
{ KEY_F1, Key::F1 },
{ KEY_F2, Key::F2 },
{ KEY_F3, Key::F3 },
{ KEY_F4, Key::F4 },
{ KEY_F5, Key::F5 },
{ KEY_F6, Key::F6 },
{ KEY_F7, Key::F7 },
{ KEY_F8, Key::F8 },
{ KEY_F9, Key::F9 },
{ KEY_F10, Key::F10 },
{ KEY_F11, Key::F11 },
{ KEY_F12, Key::F12 },
{ KEY_F13, Key::F13 },
{ KEY_F14, Key::F14 },
{ KEY_F15, Key::F15 },
{ KEY_F16, Key::F16 },
{ KEY_F17, Key::F17 },
{ KEY_F18, Key::F18 },
{ KEY_F19, Key::F19 },
{ KEY_F20, Key::F20 },
{ KEY_F21, Key::F21 },
{ KEY_F22, Key::F22 },
{ KEY_F23, Key::F23 },
{ KEY_F24, Key::F24 },
{ KEY_KP0, Key::Keypad0 },
{ KEY_KP1, Key::Keypad1 },
{ KEY_KP2, Key::Keypad2 },
{ KEY_KP3, Key::Keypad3 },
{ KEY_KP4, Key::Keypad4 },
{ KEY_KP5, Key::Keypad5 },
{ KEY_KP6, Key::Keypad6 },
{ KEY_KP7, Key::Keypad7 },
{ KEY_KP8, Key::Keypad8 },
{ KEY_KP9, Key::Keypad9 },
{ KEY_KPDOT, Key::KeypadDecimal },
{ KEY_KPSLASH, Key::KeypadDivide },
{ KEY_KPASTERISK, Key::KeypadMultiply },
{ KEY_KPMINUS, Key::KeypadSubtract },
{ KEY_KPPLUS, Key::KeypadAdd },
{ KEY_KPENTER, Key::KeypadEnter },
{ KEY_KPEQUAL, Key::KeypadEqual },
{ KEY_LEFTSHIFT, Key::LeftShift },
{ KEY_LEFTCTRL, Key::LeftControl },
{ KEY_LEFTALT, Key::LeftAlt },
{ KEY_LEFTMETA, Key::LeftSuper },
{ KEY_RIGHTSHIFT, Key::RightShift },
{ KEY_RIGHTCTRL, Key::RightControl },
{ KEY_RIGHTALT, Key::RightAlt },
{ KEY_RIGHTMETA, Key::RightSuper },
{ KEY_COMPOSE, Key::Menu },
};
return keys;
}
}
std::optional<Key> tp::keyFromEvdev(uint32_t evdevKey) {
const auto& keys = evdevKeys();
const auto found = keys.find(evdevKey);
if (found == keys.end()) {
return std::nullopt;
}
return found->second;
}
std::optional<PointerButton> tp::pointerButtonFromLinux(uint32_t linuxButton) {
switch (linuxButton) {
case BTN_LEFT: return PointerButton::Left;
case BTN_RIGHT: return PointerButton::Right;
case BTN_MIDDLE: return PointerButton::Middle;
case BTN_SIDE: return PointerButton::Back;
case BTN_EXTRA: return PointerButton::Forward;
default: return std::nullopt;
}
}

View file

@ -0,0 +1,13 @@
#pragma once
#include "InputCodes.hpp"
#include <cstdint>
#include <optional>
namespace tp {
[[nodiscard]] std::optional<Key> keyFromEvdev(uint32_t evdevKey);
[[nodiscard]] std::optional<PointerButton> pointerButtonFromLinux(uint32_t linuxButton);
}

View file

@ -0,0 +1,38 @@
#include "PointerPinchConverter.hpp"
#include <numbers>
namespace tp {
TouchGesture PointerPinchConverter::begin(Vec2F center) {
mActive = true;
mTotalScale = 1;
return { GesturePhase::Begin, center };
}
std::optional<TouchGesture>
PointerPinchConverter::update(Vec2F center, Vec2F pan, halnf totalScale, halnf rotationDegrees) {
if (!mActive) {
return std::nullopt;
}
TouchGesture gesture;
gesture.phase = GesturePhase::Update;
gesture.center = center;
gesture.pan = pan;
gesture.scale = mTotalScale > 0 && totalScale > 0 ? totalScale / mTotalScale : 1;
gesture.rotation = wrapAngle(rotationDegrees * std::numbers::pi_v<halnf> / 180);
if (totalScale > 0) {
mTotalScale = totalScale;
}
return gesture;
}
std::optional<TouchGesture> PointerPinchConverter::end(Vec2F center) {
if (!mActive) {
return std::nullopt;
}
mActive = false;
return TouchGesture{ GesturePhase::End, center };
}
}

View file

@ -0,0 +1,21 @@
#pragma once
#include "TouchGesture.hpp"
#include <optional>
namespace tp {
// Converts touchpad pinch updates (scale relative to the gesture start, pan and rotation per event) into
// incremental TouchGesture updates.
class PointerPinchConverter {
public:
[[nodiscard]] TouchGesture begin(Vec2F center);
[[nodiscard]] std::optional<TouchGesture> update(Vec2F center, Vec2F pan, halnf totalScale, halnf rotationDegrees);
[[nodiscard]] std::optional<TouchGesture> end(Vec2F center);
private:
halnf mTotalScale = 1;
bool mActive = false;
};
}

View file

@ -0,0 +1,260 @@
#include "WaylandClipboard.hpp"
#include "WaylandConnection.hpp"
#include <wayland-client.h>
#include <algorithm>
#include <cerrno>
#include <fcntl.h>
#include <unistd.h>
namespace tp {
namespace {
constexpr const char* kTextMimeType = "text/plain;charset=utf-8";
constexpr const char* kLegacyTextMimeType = "text/plain";
constexpr const char* kPngMimeType = "image/png";
constexpr const char* kImageMimePrefix = "image/";
WaylandClipboard& clipboard(void* data) { return *static_cast<WaylandClipboard*>(data); }
void offerMimeType(void* data, wl_data_offer* offer, const char* mimeType) {
clipboard(data).onOfferMimeType(offer, mimeType);
}
void offerSourceActions(void*, wl_data_offer*, uint32_t) {}
void offerAction(void*, wl_data_offer*, uint32_t) {}
void dataOffer(void* data, wl_data_device*, wl_data_offer* offer) { clipboard(data).onOffer(offer); }
void dataEnter(void*, wl_data_device*, uint32_t, wl_surface*, wl_fixed_t, wl_fixed_t, wl_data_offer* offer) {
if (offer) {
wl_data_offer_destroy(offer);
}
}
void dataLeave(void*, wl_data_device*) {}
void dataMotion(void*, wl_data_device*, uint32_t, wl_fixed_t, wl_fixed_t) {}
void dataDrop(void*, wl_data_device*) {}
void dataSelection(void* data, wl_data_device*, wl_data_offer* offer) { clipboard(data).onSelection(offer); }
void sourceTarget(void*, wl_data_source*, const char*) {}
void sourceSend(void* data, wl_data_source*, const char* mimeType, int32_t descriptor) {
clipboard(data).onSourceSend(mimeType, descriptor);
}
void sourceCancelled(void* data, wl_data_source* source) { clipboard(data).onSourceCancelled(source); }
void sourceDropPerformed(void*, wl_data_source*) {}
void sourceDndFinished(void*, wl_data_source*) {}
void sourceAction(void*, wl_data_source*, uint32_t) {}
const wl_data_offer_listener kOfferListener = { offerMimeType, offerSourceActions, offerAction };
const wl_data_device_listener kDataDeviceListener = {
dataOffer, dataEnter, dataLeave, dataMotion, dataDrop, dataSelection,
};
const wl_data_source_listener kSourceListener = {
sourceTarget, sourceSend, sourceCancelled, sourceDropPerformed, sourceDndFinished, sourceAction,
};
void writeAll(int descriptor, const std::string& text) {
size_t written = 0;
while (written < text.size()) {
const ssize_t result = write(descriptor, text.data() + written, text.size() - written);
if (result < 0 && errno == EINTR) {
continue;
}
if (result <= 0) {
return;
}
written += (size_t) result;
}
}
const std::string* preferredImageType(const std::vector<std::string>& types) {
const std::string* chosen = nullptr;
for (const std::string& type : types) {
if (type == kPngMimeType) {
return &type;
}
if (!chosen && type.starts_with(kImageMimePrefix)) {
chosen = &type;
}
}
return chosen;
}
bool offers(const std::vector<std::string>& types, const char* mimeType) {
return std::find(types.begin(), types.end(), mimeType) != types.end();
}
}
WaylandClipboard::WaylandClipboard(WaylandSeatContext& seat, wl_data_device_manager* manager, wl_seat* waylandSeat) :
mSeat(seat),
mManager(manager) {
mDevice = wl_data_device_manager_get_data_device(manager, waylandSeat);
wl_data_device_add_listener(mDevice, &kDataDeviceListener, this);
}
WaylandClipboard::~WaylandClipboard() {
closeTransfer(mText);
closeTransfer(mImage);
if (mSource) {
wl_data_source_destroy(mSource);
}
if (mSelection) {
wl_data_offer_destroy(mSelection);
}
if (mPendingOffer && mPendingOffer != mSelection) {
wl_data_offer_destroy(mPendingOffer);
}
if (mDevice) {
wl_data_device_release(mDevice);
}
}
void WaylandClipboard::setText(const std::string& text) {
mOwnedText = text;
if (mSource) {
wl_data_source_destroy(mSource);
}
mSource = wl_data_device_manager_create_data_source(mManager);
wl_data_source_add_listener(mSource, &kSourceListener, this);
wl_data_source_offer(mSource, kTextMimeType);
wl_data_source_offer(mSource, kLegacyTextMimeType);
wl_data_device_set_selection(mDevice, mSource, mSeat.lastInputSerial());
mSeat.connection().flush();
}
std::string WaylandClipboard::text() const {
if (mSource) {
return mOwnedText;
}
return mText.complete ? mText.bytes : std::string();
}
ClipboardImage WaylandClipboard::image() const {
if (mSource || !mImage.complete) {
return {};
}
return { mImage.mimeType, mImage.bytes };
}
void WaylandClipboard::startTransfer(Transfer& transfer, const std::string& mimeType) {
closeTransfer(transfer);
transfer = Transfer{ mimeType };
int pipeDescriptors[2];
if (pipe2(pipeDescriptors, O_CLOEXEC | O_NONBLOCK) != 0) {
transfer.complete = true;
return;
}
wl_data_offer_receive(mSelection, mimeType.c_str(), pipeDescriptors[1]);
close(pipeDescriptors[1]);
transfer.descriptor = pipeDescriptors[0];
}
void WaylandClipboard::readAvailable(Transfer& transfer) {
char buffer[4096];
while (true) {
const ssize_t result = read(transfer.descriptor, buffer, sizeof(buffer));
if (result > 0) {
transfer.bytes.append(buffer, (size_t) result);
continue;
}
if (result < 0 && errno == EINTR) {
continue;
}
if (result < 0 && errno == EAGAIN) {
return;
}
closeTransfer(transfer);
transfer.complete = true;
return;
}
}
void WaylandClipboard::closeTransfer(Transfer& transfer) {
if (transfer.descriptor >= 0) {
close(transfer.descriptor);
}
transfer.descriptor = -1;
}
void WaylandClipboard::collectPollDescriptors(std::vector<pollfd>& descriptors) const {
for (const Transfer* transfer : { &mText, &mImage }) {
if (transfer->descriptor >= 0) {
descriptors.push_back({ transfer->descriptor, POLLIN, 0 });
}
}
}
void WaylandClipboard::onDescriptorsReady(const std::vector<pollfd>& descriptors) {
for (const pollfd& ready : descriptors) {
if (!(ready.revents & (POLLIN | POLLHUP | POLLERR))) {
continue;
}
for (Transfer* transfer : { &mText, &mImage }) {
if (transfer->descriptor == ready.fd) {
readAvailable(*transfer);
}
}
}
}
void WaylandClipboard::forgetSelection() {
closeTransfer(mText);
closeTransfer(mImage);
mText = {};
mImage = {};
if (mSelection) {
wl_data_offer_destroy(mSelection);
}
mSelection = nullptr;
}
void WaylandClipboard::onOffer(wl_data_offer* offer) {
mPendingOffer = offer;
mPendingOfferTypes.clear();
wl_data_offer_add_listener(offer, &kOfferListener, this);
}
void WaylandClipboard::onOfferMimeType(wl_data_offer* offer, const char* mimeType) {
if (offer == mPendingOffer) {
mPendingOfferTypes.emplace_back(mimeType);
}
}
void WaylandClipboard::onSelection(wl_data_offer* offer) {
const std::vector<std::string> types =
offer && offer == mPendingOffer ? std::move(mPendingOfferTypes) : std::vector<std::string>{};
if (mSelection != offer) {
forgetSelection();
}
mSelection = offer;
mPendingOfferTypes.clear();
mPendingOffer = nullptr;
if (!mSelection) {
return;
}
if (offers(types, kTextMimeType)) {
startTransfer(mText, kTextMimeType);
}
if (const std::string* imageType = preferredImageType(types)) {
startTransfer(mImage, *imageType);
}
mSeat.connection().flush();
}
void WaylandClipboard::onSourceSend(const char*, int descriptor) {
writeAll(descriptor, mOwnedText);
close(descriptor);
}
void WaylandClipboard::onSourceCancelled(wl_data_source* source) {
if (source == mSource) {
mSource = nullptr;
}
wl_data_source_destroy(source);
}
}

View file

@ -0,0 +1,66 @@
#pragma once
#include "WaylandSeatContext.hpp"
#include "WindowTypes.hpp"
#include <poll.h>
#include <string>
#include <vector>
struct wl_data_device;
struct wl_data_device_manager;
struct wl_data_offer;
struct wl_data_source;
struct wl_seat;
namespace tp {
// The selection clipboard. Offered contents are read asynchronously on the event loop, so reading the clipboard
// never blocks the UI thread; a selection that is still arriving reads as empty.
class WaylandClipboard {
public:
WaylandClipboard(WaylandSeatContext& seat, wl_data_device_manager* manager, wl_seat* waylandSeat);
~WaylandClipboard();
WaylandClipboard(const WaylandClipboard&) = delete;
WaylandClipboard& operator=(const WaylandClipboard&) = delete;
void setText(const std::string& text);
[[nodiscard]] std::string text() const;
[[nodiscard]] ClipboardImage image() const;
void collectPollDescriptors(std::vector<pollfd>& descriptors) const;
void onDescriptorsReady(const std::vector<pollfd>& descriptors);
void onOffer(wl_data_offer* offer);
void onOfferMimeType(wl_data_offer* offer, const char* mimeType);
void onSelection(wl_data_offer* offer);
void onSourceSend(const char* mimeType, int descriptor);
void onSourceCancelled(wl_data_source* source);
private:
struct Transfer {
std::string mimeType;
int descriptor = -1;
std::string bytes;
bool complete = false;
};
void startTransfer(Transfer& transfer, const std::string& mimeType);
static void readAvailable(Transfer& transfer);
static void closeTransfer(Transfer& transfer);
void forgetSelection();
WaylandSeatContext& mSeat;
wl_data_device_manager* mManager;
wl_data_device* mDevice = nullptr;
wl_data_offer* mPendingOffer = nullptr;
std::vector<std::string> mPendingOfferTypes;
wl_data_offer* mSelection = nullptr;
wl_data_source* mSource = nullptr;
std::string mOwnedText;
Transfer mText;
Transfer mImage;
};
}

View file

@ -0,0 +1,193 @@
#include "WaylandConnection.hpp"
#include "cursor-shape-v1-client-protocol.h"
#include "pointer-gestures-unstable-v1-client-protocol.h"
#include "fractional-scale-v1-client-protocol.h"
#include "tablet-v2-client-protocol.h"
#include "text-input-unstable-v3-client-protocol.h"
#include "viewporter-client-protocol.h"
#include "xdg-activation-v1-client-protocol.h"
#include "xdg-decoration-unstable-v1-client-protocol.h"
#include "xdg-shell-client-protocol.h"
#include <wayland-client.h>
#include <algorithm>
#include <cerrno>
#include <poll.h>
#include <stdexcept>
namespace tp {
struct WaylandRegistryListener {
static void global(void* data, wl_registry* registry, uint32_t name, const char* interface, uint32_t version) {
static_cast<WaylandConnection*>(data)->bindGlobal(registry, name, interface, version);
}
static void globalRemove(void*, wl_registry*, uint32_t) {}
static void ping(void*, xdg_wm_base* windowManager, uint32_t serial) { xdg_wm_base_pong(windowManager, serial); }
};
namespace {
const wl_registry_listener kRegistryListener = {
WaylandRegistryListener::global,
WaylandRegistryListener::globalRemove,
};
const xdg_wm_base_listener kWindowManagerListener = { WaylandRegistryListener::ping };
template <typename Interface>
Interface*
bindAs(wl_registry* registry, uint32_t name, const wl_interface* interface, uint32_t offered, uint32_t wanted) {
return static_cast<Interface*>(wl_registry_bind(registry, name, interface, std::min(offered, wanted)));
}
}
WaylandConnection::WaylandConnection() {
mDisplay = wl_display_connect(nullptr);
if (!mDisplay) {
throw std::runtime_error("cannot connect to the Wayland display; this build supports Wayland only");
}
mRegistry = wl_display_get_registry(mDisplay);
wl_registry_add_listener(mRegistry, &kRegistryListener, this);
wl_display_roundtrip(mDisplay);
wl_display_roundtrip(mDisplay);
if (!mCompositor || !mWindowManager) {
throw std::runtime_error("Wayland compositor lacks wl_compositor or xdg_wm_base");
}
}
WaylandConnection::~WaylandConnection() {
if (mDecorationManager) {
zxdg_decoration_manager_v1_destroy(mDecorationManager);
}
if (mTextInputManager) {
zwp_text_input_manager_v3_destroy(mTextInputManager);
}
if (mActivation) {
xdg_activation_v1_destroy(mActivation);
}
if (mTabletManager) {
zwp_tablet_manager_v2_destroy(mTabletManager);
}
if (mCursorShapeManager) {
wp_cursor_shape_manager_v1_destroy(mCursorShapeManager);
}
if (mPointerGestures) {
zwp_pointer_gestures_v1_destroy(mPointerGestures);
}
if (mFractionalScaleManager) {
wp_fractional_scale_manager_v1_destroy(mFractionalScaleManager);
}
if (mViewporter) {
wp_viewporter_destroy(mViewporter);
}
if (mDataDeviceManager) {
wl_data_device_manager_destroy(mDataDeviceManager);
}
if (mSeat) {
wl_seat_release(mSeat);
}
if (mWindowManager) {
xdg_wm_base_destroy(mWindowManager);
}
if (mCompositor) {
wl_compositor_destroy(mCompositor);
}
if (mRegistry) {
wl_registry_destroy(mRegistry);
}
if (mDisplay) {
wl_display_flush(mDisplay);
wl_display_disconnect(mDisplay);
}
}
void
WaylandConnection::bindGlobal(wl_registry* registry, uint32_t name, const std::string& interface, uint32_t version) {
if (interface == wl_compositor_interface.name) {
mCompositor = bindAs<wl_compositor>(registry, name, &wl_compositor_interface, version, 4);
} else if (interface == xdg_wm_base_interface.name) {
mWindowManager = bindAs<xdg_wm_base>(registry, name, &xdg_wm_base_interface, version, 5);
xdg_wm_base_add_listener(mWindowManager, &kWindowManagerListener, this);
} else if (interface == wl_seat_interface.name && mSeatName == 0) {
mSeatName = name;
mSeatVersion = version;
} else if (interface == wl_data_device_manager_interface.name) {
mDataDeviceManager =
bindAs<wl_data_device_manager>(registry, name, &wl_data_device_manager_interface, version, 3);
} else if (interface == wp_viewporter_interface.name) {
mViewporter = bindAs<wp_viewporter>(registry, name, &wp_viewporter_interface, version, 1);
} else if (interface == wp_fractional_scale_manager_v1_interface.name) {
mFractionalScaleManager =
bindAs<wp_fractional_scale_manager_v1>(registry, name, &wp_fractional_scale_manager_v1_interface, version, 1);
} else if (interface == zwp_pointer_gestures_v1_interface.name) {
mPointerGestures =
bindAs<zwp_pointer_gestures_v1>(registry, name, &zwp_pointer_gestures_v1_interface, version, 1);
} else if (interface == wp_cursor_shape_manager_v1_interface.name) {
mCursorShapeManager =
bindAs<wp_cursor_shape_manager_v1>(registry, name, &wp_cursor_shape_manager_v1_interface, version, 1);
} else if (interface == zwp_text_input_manager_v3_interface.name) {
mTextInputManager =
bindAs<zwp_text_input_manager_v3>(registry, name, &zwp_text_input_manager_v3_interface, version, 1);
} else if (interface == zxdg_decoration_manager_v1_interface.name) {
mDecorationManager =
bindAs<zxdg_decoration_manager_v1>(registry, name, &zxdg_decoration_manager_v1_interface, version, 1);
} else if (interface == zwp_tablet_manager_v2_interface.name) {
mTabletManager = bindAs<zwp_tablet_manager_v2>(registry, name, &zwp_tablet_manager_v2_interface, version, 1);
} else if (interface == xdg_activation_v1_interface.name) {
mActivation = bindAs<xdg_activation_v1>(registry, name, &xdg_activation_v1_interface, version, 1);
}
}
wl_seat* WaylandConnection::bindSeat(const void* listener, void* data) {
if (mSeat || mSeatName == 0) {
return mSeat;
}
mSeat = bindAs<wl_seat>(mRegistry, mSeatName, &wl_seat_interface, mSeatVersion, 8);
wl_seat_add_listener(mSeat, static_cast<const wl_seat_listener*>(listener), data);
return mSeat;
}
bool WaylandConnection::dispatch(int timeoutMs) {
if (wl_display_dispatch_pending(mDisplay) > 0) {
flush();
return true;
}
while (wl_display_prepare_read(mDisplay) != 0) {
if (wl_display_dispatch_pending(mDisplay) > 0) {
return true;
}
}
flush();
pollfd descriptor = { wl_display_get_fd(mDisplay), POLLIN, 0 };
const int ready = poll(&descriptor, 1, timeoutMs);
if (ready <= 0) {
wl_display_cancel_read(mDisplay);
return false;
}
if (wl_display_read_events(mDisplay) < 0) {
throw std::runtime_error("lost the Wayland connection");
}
wl_display_dispatch_pending(mDisplay);
flush();
return true;
}
void WaylandConnection::roundtrip() { wl_display_roundtrip(mDisplay); }
void WaylandConnection::flush() {
while (wl_display_flush(mDisplay) < 0 && errno == EAGAIN) {
pollfd descriptor = { wl_display_get_fd(mDisplay), POLLOUT, 0 };
poll(&descriptor, 1, -1);
}
}
}

View file

@ -0,0 +1,72 @@
#pragma once
#include <cstdint>
#include <string>
struct wl_display;
struct wl_registry;
struct wl_compositor;
struct wl_seat;
struct wl_data_device_manager;
struct xdg_wm_base;
struct wp_viewporter;
struct wp_fractional_scale_manager_v1;
struct wp_cursor_shape_manager_v1;
struct zwp_pointer_gestures_v1;
struct zwp_text_input_manager_v3;
struct zxdg_decoration_manager_v1;
struct zwp_tablet_manager_v2;
struct xdg_activation_v1;
namespace tp {
class WaylandConnection {
public:
WaylandConnection();
~WaylandConnection();
WaylandConnection(const WaylandConnection&) = delete;
WaylandConnection& operator=(const WaylandConnection&) = delete;
[[nodiscard]] wl_display* display() const { return mDisplay; }
[[nodiscard]] wl_compositor* compositor() const { return mCompositor; }
[[nodiscard]] xdg_wm_base* windowManager() const { return mWindowManager; }
[[nodiscard]] wl_seat* seat() const { return mSeat; }
[[nodiscard]] wl_seat* bindSeat(const void* listener, void* data);
[[nodiscard]] wl_data_device_manager* dataDeviceManager() const { return mDataDeviceManager; }
[[nodiscard]] wp_viewporter* viewporter() const { return mViewporter; }
[[nodiscard]] wp_fractional_scale_manager_v1* fractionalScaleManager() const { return mFractionalScaleManager; }
[[nodiscard]] wp_cursor_shape_manager_v1* cursorShapeManager() const { return mCursorShapeManager; }
[[nodiscard]] zwp_pointer_gestures_v1* pointerGestures() const { return mPointerGestures; }
[[nodiscard]] zwp_text_input_manager_v3* textInputManager() const { return mTextInputManager; }
[[nodiscard]] zxdg_decoration_manager_v1* decorationManager() const { return mDecorationManager; }
[[nodiscard]] zwp_tablet_manager_v2* tabletManager() const { return mTabletManager; }
[[nodiscard]] xdg_activation_v1* activation() const { return mActivation; }
bool dispatch(int timeoutMs);
void roundtrip();
void flush();
private:
friend struct WaylandRegistryListener;
void bindGlobal(wl_registry* registry, uint32_t name, const std::string& interface, uint32_t version);
wl_display* mDisplay = nullptr;
wl_registry* mRegistry = nullptr;
wl_compositor* mCompositor = nullptr;
xdg_wm_base* mWindowManager = nullptr;
wl_seat* mSeat = nullptr;
uint32_t mSeatName = 0;
uint32_t mSeatVersion = 0;
wl_data_device_manager* mDataDeviceManager = nullptr;
wp_viewporter* mViewporter = nullptr;
wp_fractional_scale_manager_v1* mFractionalScaleManager = nullptr;
wp_cursor_shape_manager_v1* mCursorShapeManager = nullptr;
zwp_pointer_gestures_v1* mPointerGestures = nullptr;
zwp_text_input_manager_v3* mTextInputManager = nullptr;
zxdg_decoration_manager_v1* mDecorationManager = nullptr;
zwp_tablet_manager_v2* mTabletManager = nullptr;
xdg_activation_v1* mActivation = nullptr;
};
}

View file

@ -0,0 +1,262 @@
#include "WaylandKeyboard.hpp"
#include "LinuxInputCodes.hpp"
#include <wayland-client.h>
#include <cstdlib>
#include <sys/mman.h>
#include <sys/timerfd.h>
#include <unistd.h>
namespace tp {
namespace {
WaylandKeyboard& keyboard(void* data) { return *static_cast<WaylandKeyboard*>(data); }
void keymap(void* data, wl_keyboard*, uint32_t format, int32_t descriptor, uint32_t size) {
keyboard(data).onKeymap(format, descriptor, size);
}
void keyEnter(void* data, wl_keyboard*, uint32_t serial, wl_surface*, wl_array* keys) {
keyboard(data).onEnter(serial, keys);
}
void keyLeave(void* data, wl_keyboard*, uint32_t, wl_surface*) { keyboard(data).onLeave(); }
void key(void* data, wl_keyboard*, uint32_t serial, uint32_t, uint32_t key, uint32_t state) {
keyboard(data).onKey(serial, key, state);
}
void modifiers(
void* data, wl_keyboard*, uint32_t, uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group
) {
keyboard(data).onModifiers(depressed, latched, locked, group);
}
void repeatInfo(void* data, wl_keyboard*, int32_t rate, int32_t delay) { keyboard(data).onRepeatInfo(rate, delay); }
const wl_keyboard_listener kKeyboardListener = { keymap, keyEnter, keyLeave, key, modifiers, repeatInfo };
timespec millisecondsToTimespec(int32_t milliseconds) {
return { milliseconds / 1000, (long) (milliseconds % 1000) * 1000000L };
}
}
WaylandKeyboard::WaylandKeyboard(WaylandSeatContext& seat, wl_keyboard* keyboard) :
mSeat(seat),
mKeyboard(keyboard) {
mContext = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
mRepeatTimer = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
const char* locale = std::getenv("LC_ALL");
if (!locale || !*locale) {
locale = std::getenv("LC_CTYPE");
}
if (!locale || !*locale) {
locale = std::getenv("LANG");
}
if (!locale || !*locale) {
locale = "C";
}
mComposeTable = xkb_compose_table_new_from_locale(mContext, locale, XKB_COMPOSE_COMPILE_NO_FLAGS);
if (mComposeTable) {
mComposeState = xkb_compose_state_new(mComposeTable, XKB_COMPOSE_STATE_NO_FLAGS);
}
wl_keyboard_add_listener(mKeyboard, &kKeyboardListener, this);
}
WaylandKeyboard::~WaylandKeyboard() {
if (mRepeatTimer >= 0) {
close(mRepeatTimer);
}
if (mComposeState) {
xkb_compose_state_unref(mComposeState);
}
if (mComposeTable) {
xkb_compose_table_unref(mComposeTable);
}
if (mState) {
xkb_state_unref(mState);
}
if (mKeymap) {
xkb_keymap_unref(mKeymap);
}
if (mContext) {
xkb_context_unref(mContext);
}
wl_keyboard_release(mKeyboard);
}
void WaylandKeyboard::onKeymap(uint32_t format, int descriptor, uint32_t size) {
if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
close(descriptor);
return;
}
void* mapped = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, descriptor, 0);
close(descriptor);
if (mapped == MAP_FAILED) {
return;
}
xkb_keymap* keymap = xkb_keymap_new_from_string(
mContext, static_cast<const char*>(mapped), XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS
);
munmap(mapped, size);
if (!keymap) {
return;
}
if (mState) {
xkb_state_unref(mState);
}
if (mKeymap) {
xkb_keymap_unref(mKeymap);
}
mKeymap = keymap;
mState = xkb_state_new(mKeymap);
}
void WaylandKeyboard::onEnter(uint32_t serial, wl_array* keys) {
mSeat.recordSerial(serial);
mSeat.input().post(KeyboardFocusChanged{ true });
const auto* pressed = static_cast<const uint32_t*>(keys->data);
const size_t count = keys->size / sizeof(uint32_t);
for (size_t index = 0; index < count; index++) {
if (const std::optional<Key> key = keyFromEvdev(pressed[index])) {
mSeat.input().post(KeyChanged{ *key, KeyAction::Press });
}
}
}
void WaylandKeyboard::onLeave() {
disarmRepeat();
if (mComposeState) {
xkb_compose_state_reset(mComposeState);
}
mSeat.input().post(KeyboardFocusChanged{ false });
}
void WaylandKeyboard::onKey(uint32_t serial, uint32_t key, uint32_t state) {
mSeat.recordSerial(serial);
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
pressKey(key);
} else {
releaseKey(key);
}
}
void WaylandKeyboard::pressKey(uint32_t key) {
if (const std::optional<Key> id = keyFromEvdev(key)) {
mSeat.input().post(KeyChanged{ *id, KeyAction::Press });
}
emitText(key);
if (mKeymap && xkb_keymap_key_repeats(mKeymap, key + 8)) {
armRepeat(key);
}
}
void WaylandKeyboard::releaseKey(uint32_t key) {
if (key == mRepeatKey) {
disarmRepeat();
}
if (const std::optional<Key> id = keyFromEvdev(key)) {
mSeat.input().post(KeyChanged{ *id, KeyAction::Release });
}
}
void WaylandKeyboard::emitText(uint32_t key) {
if (!mState) {
return;
}
const xkb_keysym_t symbol = composed(xkb_state_key_get_one_sym(mState, key + 8));
if (symbol == XKB_KEY_NoSymbol) {
return;
}
char utf8[64] = {};
if (mComposeState && xkb_compose_state_get_status(mComposeState) == XKB_COMPOSE_COMPOSED) {
xkb_compose_state_get_utf8(mComposeState, utf8, sizeof(utf8));
xkb_compose_state_reset(mComposeState);
} else {
xkb_state_key_get_utf8(mState, key + 8, utf8, sizeof(utf8));
}
const unsigned char first = (unsigned char) utf8[0];
if (first == 0 || first < 0x20 || first == 0x7f) {
return;
}
mSeat.input().post(TextEntered{ utf8 });
}
xkb_keysym_t WaylandKeyboard::composed(xkb_keysym_t symbol) {
if (!mComposeState || symbol == XKB_KEY_NoSymbol) {
return symbol;
}
if (xkb_compose_state_feed(mComposeState, symbol) != XKB_COMPOSE_FEED_ACCEPTED) {
return symbol;
}
switch (xkb_compose_state_get_status(mComposeState)) {
case XKB_COMPOSE_COMPOSING: return XKB_KEY_NoSymbol;
case XKB_COMPOSE_CANCELLED: xkb_compose_state_reset(mComposeState); return XKB_KEY_NoSymbol;
case XKB_COMPOSE_COMPOSED: return xkb_compose_state_get_one_sym(mComposeState);
case XKB_COMPOSE_NOTHING: return symbol;
}
return symbol;
}
void WaylandKeyboard::onModifiers(uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group) {
if (mState) {
xkb_state_update_mask(mState, depressed, latched, locked, 0, 0, group);
}
}
void WaylandKeyboard::onRepeatInfo(int32_t rate, int32_t delay) {
mRepeatRate = rate;
mRepeatDelay = delay;
}
void WaylandKeyboard::armRepeat(uint32_t key) {
if (mRepeatTimer < 0 || mRepeatRate <= 0) {
return;
}
mRepeatKey = key;
itimerspec timer = {};
timer.it_value = millisecondsToTimespec(mRepeatDelay);
timer.it_interval = millisecondsToTimespec(1000 / mRepeatRate);
timerfd_settime(mRepeatTimer, 0, &timer, nullptr);
}
void WaylandKeyboard::disarmRepeat() {
mRepeatKey = 0;
if (mRepeatTimer < 0) {
return;
}
itimerspec timer = {};
timerfd_settime(mRepeatTimer, 0, &timer, nullptr);
}
void WaylandKeyboard::onRepeatTimer() {
uint64_t expirations = 0;
if (read(mRepeatTimer, &expirations, sizeof(expirations)) != sizeof(expirations)) {
return;
}
if (mRepeatKey == 0) {
return;
}
const std::optional<Key> id = keyFromEvdev(mRepeatKey);
for (uint64_t repeat = 0; repeat < expirations; repeat++) {
if (id) {
mSeat.input().post(KeyChanged{ *id, KeyAction::Repeat });
}
emitText(mRepeatKey);
}
}
}

View file

@ -0,0 +1,54 @@
#pragma once
#include "WaylandSeatContext.hpp"
#include <xkbcommon/xkbcommon-compose.h>
#include <xkbcommon/xkbcommon.h>
#include <cstdint>
struct wl_keyboard;
struct wl_array;
namespace tp {
// wl_keyboard with xkb keymap, compose sequences and client-side key repeat.
class WaylandKeyboard {
public:
WaylandKeyboard(WaylandSeatContext& seat, wl_keyboard* keyboard);
~WaylandKeyboard();
WaylandKeyboard(const WaylandKeyboard&) = delete;
WaylandKeyboard& operator=(const WaylandKeyboard&) = delete;
[[nodiscard]] int repeatTimerDescriptor() const { return mRepeatTimer; }
void onRepeatTimer();
void onKeymap(uint32_t format, int descriptor, uint32_t size);
void onEnter(uint32_t serial, wl_array* keys);
void onLeave();
void onKey(uint32_t serial, uint32_t key, uint32_t state);
void onModifiers(uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group);
void onRepeatInfo(int32_t rate, int32_t delay);
private:
void pressKey(uint32_t key);
void releaseKey(uint32_t key);
void emitText(uint32_t key);
void armRepeat(uint32_t key);
void disarmRepeat();
[[nodiscard]] xkb_keysym_t composed(xkb_keysym_t symbol);
WaylandSeatContext& mSeat;
wl_keyboard* mKeyboard;
xkb_context* mContext = nullptr;
xkb_keymap* mKeymap = nullptr;
xkb_state* mState = nullptr;
xkb_compose_table* mComposeTable = nullptr;
xkb_compose_state* mComposeState = nullptr;
int mRepeatTimer = -1;
int32_t mRepeatRate = 25;
int32_t mRepeatDelay = 600;
uint32_t mRepeatKey = 0;
};
}

View file

@ -0,0 +1,231 @@
#include "WaylandPointer.hpp"
#include "WaylandConnection.hpp"
#include "cursor-shape-v1-client-protocol.h"
#include "pointer-gestures-unstable-v1-client-protocol.h"
namespace tp {
namespace {
constexpr halnf kWheelStep = 10;
WaylandPointer& pointer(void* data) { return *static_cast<WaylandPointer*>(data); }
void pointerEnter(void* data, wl_pointer*, uint32_t serial, wl_surface*, wl_fixed_t x, wl_fixed_t y) {
pointer(data).onEnter(serial, x, y);
}
void pointerLeave(void* data, wl_pointer*, uint32_t, wl_surface*) { pointer(data).onLeave(); }
void pointerMotion(void* data, wl_pointer*, uint32_t, wl_fixed_t x, wl_fixed_t y) { pointer(data).onMotion(x, y); }
void pointerButton(void* data, wl_pointer*, uint32_t serial, uint32_t, uint32_t button, uint32_t state) {
pointer(data).onButton(serial, button, state);
}
void pointerAxis(void* data, wl_pointer*, uint32_t, uint32_t axis, wl_fixed_t value) {
pointer(data).onAxis(axis, value);
}
void pointerFrame(void* data, wl_pointer*) { pointer(data).onFrame(); }
void pointerAxisSource(void* data, wl_pointer*, uint32_t source) { pointer(data).onAxisSource(source); }
void pointerAxisStop(void* data, wl_pointer*, uint32_t, uint32_t) { pointer(data).onAxisStop(); }
void pointerAxisDiscrete(void* data, wl_pointer*, uint32_t axis, int32_t steps) {
pointer(data).onAxisDiscrete(axis, steps);
}
void pointerAxisValue120(void*, wl_pointer*, uint32_t, int32_t) {}
void pointerAxisRelativeDirection(void*, wl_pointer*, uint32_t, uint32_t) {}
const wl_pointer_listener kPointerListener = {
pointerEnter,
pointerLeave,
pointerMotion,
pointerButton,
pointerAxis,
pointerFrame,
pointerAxisSource,
pointerAxisStop,
pointerAxisDiscrete,
pointerAxisValue120,
pointerAxisRelativeDirection,
};
void pinchBegin(void* data, zwp_pointer_gesture_pinch_v1*, uint32_t, uint32_t, wl_surface*, uint32_t) {
pointer(data).onPinchBegin();
}
void pinchUpdate(
void* data,
zwp_pointer_gesture_pinch_v1*,
uint32_t,
wl_fixed_t dx,
wl_fixed_t dy,
wl_fixed_t scale,
wl_fixed_t rotation
) {
pointer(data).onPinchUpdate(dx, dy, scale, rotation);
}
void pinchEnd(void* data, zwp_pointer_gesture_pinch_v1*, uint32_t, uint32_t, int32_t) {
pointer(data).onPinchEnd();
}
const zwp_pointer_gesture_pinch_v1_listener kPinchListener = { pinchBegin, pinchUpdate, pinchEnd };
}
uint32_t cursorShapeCode(CursorShape shape) {
switch (shape) {
case CursorShape::Default: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT;
case CursorShape::Pointer: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER;
case CursorShape::Text: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT;
case CursorShape::Move: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE;
case CursorShape::Grab: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRAB;
case CursorShape::Grabbing: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRABBING;
case CursorShape::ResizeHorizontal: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE;
case CursorShape::ResizeVertical: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE;
case CursorShape::ResizeNorthWestSouthEast: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NWSE_RESIZE;
case CursorShape::ResizeNorthEastSouthWest: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NESW_RESIZE;
case CursorShape::NotAllowed: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NOT_ALLOWED;
case CursorShape::Crosshair: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR;
case CursorShape::Hidden: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT;
}
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT;
}
WaylandPointer::WaylandPointer(WaylandSeatContext& seat, wl_pointer* pointer) :
mSeat(seat),
mPointer(pointer) {
wl_pointer_add_listener(mPointer, &kPointerListener, this);
wp_cursor_shape_manager_v1* manager = seat.connection().cursorShapeManager();
if (manager) {
mShapeDevice = wp_cursor_shape_manager_v1_get_pointer(manager, mPointer);
}
zwp_pointer_gestures_v1* gestures = seat.connection().pointerGestures();
if (gestures) {
mPinch = zwp_pointer_gestures_v1_get_pinch_gesture(gestures, mPointer);
zwp_pointer_gesture_pinch_v1_add_listener(mPinch, &kPinchListener, this);
}
}
WaylandPointer::~WaylandPointer() {
if (mPinch) {
zwp_pointer_gesture_pinch_v1_destroy(mPinch);
}
if (mShapeDevice) {
wp_cursor_shape_device_v1_destroy(mShapeDevice);
}
wl_pointer_release(mPointer);
}
void WaylandPointer::onPinchBegin() {
mSeat.surfacePointer().onPointerSurfacePinch(mPinchConverter.begin(mSurfacePosition));
}
void WaylandPointer::onPinchUpdate(wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale, wl_fixed_t rotation) {
const Vec2F pan = { (halnf) wl_fixed_to_double(dx), (halnf) wl_fixed_to_double(dy) };
const std::optional<TouchGesture> gesture = mPinchConverter.update(
mSurfacePosition, pan, (halnf) wl_fixed_to_double(scale), (halnf) wl_fixed_to_double(rotation)
);
if (gesture) {
mSeat.surfacePointer().onPointerSurfacePinch(*gesture);
}
}
void WaylandPointer::onPinchEnd() {
if (const std::optional<TouchGesture> gesture = mPinchConverter.end(mSurfacePosition)) {
mSeat.surfacePointer().onPointerSurfacePinch(*gesture);
}
}
void WaylandPointer::onEnter(uint32_t serial, wl_fixed_t x, wl_fixed_t y) {
mEnterSerial = serial;
mInside = true;
mSurfacePosition = { (halnf) wl_fixed_to_double(x), (halnf) wl_fixed_to_double(y) };
mShapeApplied = false;
mSeat.surfacePointer().onPointerSurfaceMove({ (halnf) wl_fixed_to_double(x), (halnf) wl_fixed_to_double(y) });
applyCursor();
}
void WaylandPointer::onLeave() {
mInside = false;
mSeat.surfacePointer().onPointerSurfaceLeft();
}
void WaylandPointer::onMotion(wl_fixed_t x, wl_fixed_t y) {
mSurfacePosition = { (halnf) wl_fixed_to_double(x), (halnf) wl_fixed_to_double(y) };
mSeat.input().post(PointerSampled{});
mSeat.surfacePointer().onPointerSurfaceMove(mSurfacePosition);
}
void WaylandPointer::onButton(uint32_t serial, uint32_t button, uint32_t state) {
mSeat.recordSerial(serial);
mSeat.input().post(PointerSampled{});
mSeat.surfacePointer().onPointerSurfaceButton(button, state == WL_POINTER_BUTTON_STATE_PRESSED, serial);
}
void WaylandPointer::onAxis(uint32_t axis, wl_fixed_t value) {
if (mDiscreteScroll) {
return;
}
const halnf amount = (halnf) -wl_fixed_to_double(value) / kWheelStep;
if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) {
mPendingScroll.y += amount;
} else {
mPendingScroll.x += amount;
}
}
void WaylandPointer::onAxisDiscrete(uint32_t axis, int32_t steps) {
mDiscreteScroll = true;
if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) {
mPendingScroll.y -= (halnf) steps;
} else {
mPendingScroll.x -= (halnf) steps;
}
}
void WaylandPointer::onAxisSource(uint32_t source) {
const bool continuous = source == WL_POINTER_AXIS_SOURCE_FINGER || source == WL_POINTER_AXIS_SOURCE_CONTINUOUS;
mScrollSource = continuous ? ScrollSource::Touchpad : ScrollSource::Wheel;
}
void WaylandPointer::onAxisStop() { mScrollStopped = true; }
void WaylandPointer::onFrame() {
InputSink& input = mSeat.input();
if (mPendingScroll.x != 0 || mPendingScroll.y != 0) {
input.post(Scrolled{ mPendingScroll, mScrollSource });
}
if (mScrollStopped) {
input.post(ScrollEnded{});
}
mPendingScroll = { 0, 0 };
mDiscreteScroll = false;
mScrollStopped = false;
mScrollSource = ScrollSource::Wheel;
}
void WaylandPointer::setCursor(CursorShape shape) {
if (shape == mShape && mShapeApplied) {
return;
}
mShape = shape;
mShapeApplied = false;
applyCursor();
}
void WaylandPointer::applyCursor() {
if (!mInside || mShapeApplied) {
return;
}
if (mShape == CursorShape::Hidden) {
wl_pointer_set_cursor(mPointer, mEnterSerial, nullptr, 0, 0);
mShapeApplied = true;
return;
}
if (!mShapeDevice) {
return;
}
wp_cursor_shape_device_v1_set_shape(mShapeDevice, mEnterSerial, cursorShapeCode(mShape));
mShapeApplied = true;
}
}

View file

@ -0,0 +1,59 @@
#pragma once
#include "PointerPinchConverter.hpp"
#include "WaylandSeatContext.hpp"
#include "WindowTypes.hpp"
#include <wayland-client.h>
struct wp_cursor_shape_device_v1;
struct zwp_pointer_gesture_pinch_v1;
namespace tp {
// wl_pointer with cursor shapes, frame-grouped scrolling and touchpad pinches.
class WaylandPointer {
public:
WaylandPointer(WaylandSeatContext& seat, wl_pointer* pointer);
~WaylandPointer();
WaylandPointer(const WaylandPointer&) = delete;
WaylandPointer& operator=(const WaylandPointer&) = delete;
void setCursor(CursorShape shape);
void onEnter(uint32_t serial, wl_fixed_t x, wl_fixed_t y);
void onLeave();
void onMotion(wl_fixed_t x, wl_fixed_t y);
void onButton(uint32_t serial, uint32_t button, uint32_t state);
void onAxis(uint32_t axis, wl_fixed_t value);
void onAxisDiscrete(uint32_t axis, int32_t steps);
void onAxisSource(uint32_t source);
void onAxisStop();
void onFrame();
void onPinchBegin();
void onPinchUpdate(wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale, wl_fixed_t rotation);
void onPinchEnd();
private:
void applyCursor();
WaylandSeatContext& mSeat;
wl_pointer* mPointer;
zwp_pointer_gesture_pinch_v1* mPinch = nullptr;
PointerPinchConverter mPinchConverter;
Vec2F mSurfacePosition = { 0, 0 };
wp_cursor_shape_device_v1* mShapeDevice = nullptr;
uint32_t mEnterSerial = 0;
bool mInside = false;
CursorShape mShape = CursorShape::Default;
bool mShapeApplied = false;
Vec2F mPendingScroll = { 0, 0 };
bool mDiscreteScroll = false;
ScrollSource mScrollSource = ScrollSource::Wheel;
bool mScrollStopped = false;
};
[[nodiscard]] uint32_t cursorShapeCode(CursorShape shape);
}

View file

@ -0,0 +1,113 @@
#include "WaylandSeat.hpp"
#include "WaylandClipboard.hpp"
#include "WaylandConnection.hpp"
#include "WaylandKeyboard.hpp"
#include "WaylandPointer.hpp"
#include "WaylandTablet.hpp"
#include "WaylandTextInput.hpp"
#include <wayland-client.h>
namespace tp {
namespace {
void capabilities(void* data, wl_seat*, uint32_t capabilities) {
static_cast<WaylandSeat*>(data)->onCapabilities(capabilities);
}
void name(void*, wl_seat*, const char*) {}
const wl_seat_listener kSeatListener = { capabilities, name };
}
WaylandSeat::WaylandSeat(
WaylandConnection& connection, PlatformWindowHost& host, WaylandSurfacePointer& surfacePointer
) :
mContext(connection, host, surfacePointer) {
if (!connection.bindSeat(&kSeatListener, this)) {
return;
}
if (connection.dataDeviceManager()) {
mClipboard = std::make_unique<WaylandClipboard>(mContext, connection.dataDeviceManager(), connection.seat());
}
if (connection.textInputManager()) {
mTextInput = std::make_unique<WaylandTextInput>(mContext, connection.textInputManager(), connection.seat());
}
if (connection.tabletManager()) {
mTablet = std::make_unique<WaylandTablet>(mContext, connection.tabletManager(), connection.seat());
}
}
WaylandSeat::~WaylandSeat() = default;
void WaylandSeat::onCapabilities(uint32_t capabilities) {
const bool hasKeyboard = capabilities & WL_SEAT_CAPABILITY_KEYBOARD;
const bool hasPointer = capabilities & WL_SEAT_CAPABILITY_POINTER;
mHasTouch = capabilities & WL_SEAT_CAPABILITY_TOUCH;
wl_seat* seat = mContext.connection().seat();
if (hasKeyboard && !mKeyboard) {
mKeyboard = std::make_unique<WaylandKeyboard>(mContext, wl_seat_get_keyboard(seat));
}
if (!hasKeyboard) {
mKeyboard.reset();
}
if (hasPointer && !mPointer) {
mPointer = std::make_unique<WaylandPointer>(mContext, wl_seat_get_pointer(seat));
}
if (!hasPointer) {
mPointer.reset();
}
}
void WaylandSeat::collectPollDescriptors(std::vector<pollfd>& descriptors) const {
if (mKeyboard && mKeyboard->repeatTimerDescriptor() >= 0) {
descriptors.push_back({ mKeyboard->repeatTimerDescriptor(), POLLIN, 0 });
}
if (mClipboard) {
mClipboard->collectPollDescriptors(descriptors);
}
}
void WaylandSeat::onDescriptorsReady(const std::vector<pollfd>& descriptors) {
for (const pollfd& ready : descriptors) {
if (mKeyboard && ready.fd == mKeyboard->repeatTimerDescriptor() && (ready.revents & POLLIN)) {
mKeyboard->onRepeatTimer();
}
}
if (mClipboard) {
mClipboard->onDescriptorsReady(descriptors);
}
}
void WaylandSeat::setCursor(CursorShape shape) {
if (mPointer) {
mPointer->setCursor(shape);
}
}
void WaylandSeat::setClipboardText(const std::string& text) {
if (mClipboard) {
mClipboard->setText(text);
}
}
std::string WaylandSeat::clipboardText() const { return mClipboard ? mClipboard->text() : std::string(); }
ClipboardImage WaylandSeat::clipboardImage() const { return mClipboard ? mClipboard->image() : ClipboardImage{}; }
void WaylandSeat::setTextInputEnabled(bool enabled) {
if (mTextInput) {
mTextInput->setEnabled(enabled);
}
}
void WaylandSeat::setTextInputRect(const RectF& surfaceRect) {
if (mTextInput) {
mTextInput->setCursorRect(surfaceRect);
}
}
}

View file

@ -0,0 +1,61 @@
#pragma once
#include "Rect.hpp"
#include "WaylandSeatContext.hpp"
#include "WindowTypes.hpp"
#include <poll.h>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
namespace tp {
class WaylandClipboard;
class WaylandKeyboard;
class WaylandPointer;
class WaylandTablet;
class WaylandTextInput;
// The wl_seat and the input devices it currently offers.
class WaylandSeat {
public:
WaylandSeat(WaylandConnection& connection, PlatformWindowHost& host, WaylandSurfacePointer& surfacePointer);
~WaylandSeat();
WaylandSeat(const WaylandSeat&) = delete;
WaylandSeat& operator=(const WaylandSeat&) = delete;
void collectPollDescriptors(std::vector<pollfd>& descriptors) const;
void onDescriptorsReady(const std::vector<pollfd>& descriptors);
[[nodiscard]] uint32_t lastInputSerial() const { return mContext.lastInputSerial(); }
void setCursor(CursorShape shape);
void setClipboardText(const std::string& text);
[[nodiscard]] std::string clipboardText() const;
[[nodiscard]] ClipboardImage clipboardImage() const;
void setTextInputEnabled(bool enabled);
void setTextInputRect(const RectF& surfaceRect);
[[nodiscard]] bool hasPointer() const { return mPointer != nullptr; }
[[nodiscard]] bool hasKeyboard() const { return mKeyboard != nullptr; }
[[nodiscard]] bool hasTouch() const { return mHasTouch; }
[[nodiscard]] bool hasTablet() const { return mTablet != nullptr; }
void onCapabilities(uint32_t capabilities);
private:
WaylandSeatContext mContext;
std::unique_ptr<WaylandKeyboard> mKeyboard;
std::unique_ptr<WaylandPointer> mPointer;
std::unique_ptr<WaylandClipboard> mClipboard;
std::unique_ptr<WaylandTextInput> mTextInput;
std::unique_ptr<WaylandTablet> mTablet;
bool mHasTouch = false;
};
}

View file

@ -0,0 +1,44 @@
#pragma once
#include "PlatformWindow.hpp"
#include "TouchGesture.hpp"
#include <cstdint>
namespace tp {
class WaylandConnection;
// Pointer-like devices report in surface coordinates; the window maps them to content or decoration edges.
class WaylandSurfacePointer {
public:
virtual ~WaylandSurfacePointer() = default;
virtual void onPointerSurfaceMove(Vec2F surfacePoint) = 0;
virtual void onPointerSurfaceButton(uint32_t linuxButton, bool pressed, uint32_t serial) = 0;
virtual void onPointerSurfaceLeft() = 0;
virtual void onPointerSurfacePinch(const TouchGesture& gesture) = 0;
};
// What every seat device shares: the connection, the raw input entry, surface pointer routing and the last serial.
class WaylandSeatContext {
public:
WaylandSeatContext(WaylandConnection& connection, PlatformWindowHost& host, WaylandSurfacePointer& surfacePointer) :
mConnection(connection),
mHost(host),
mSurfacePointer(surfacePointer) {}
[[nodiscard]] WaylandConnection& connection() const { return mConnection; }
[[nodiscard]] InputSink& input() const { return mHost.rawInput(); }
[[nodiscard]] WaylandSurfacePointer& surfacePointer() const { return mSurfacePointer; }
void recordSerial(uint32_t serial) { mLastInputSerial = serial; }
[[nodiscard]] uint32_t lastInputSerial() const { return mLastInputSerial; }
private:
WaylandConnection& mConnection;
PlatformWindowHost& mHost;
WaylandSurfacePointer& mSurfacePointer;
uint32_t mLastInputSerial = 0;
};
}

View file

@ -0,0 +1,210 @@
#include "WaylandTablet.hpp"
#include "WaylandConnection.hpp"
#include "cursor-shape-v1-client-protocol.h"
#include "tablet-v2-client-protocol.h"
#include <wayland-client.h>
#include <linux/input-event-codes.h>
#include <algorithm>
namespace tp {
namespace {
WaylandTablet& tablet(void* data) { return *static_cast<WaylandTablet*>(data); }
WaylandTablet::Tool& tool(void* data) { return *static_cast<WaylandTablet::Tool*>(data); }
void tabletAdded(void* data, zwp_tablet_seat_v2*, zwp_tablet_v2* id) { tablet(data).onTabletAdded(id); }
void toolAdded(void* data, zwp_tablet_seat_v2*, zwp_tablet_tool_v2* id) { tablet(data).onToolAdded(id); }
void padAdded(void* data, zwp_tablet_seat_v2*, zwp_tablet_pad_v2* id) { tablet(data).onPadAdded(id); }
void tabletName(void*, zwp_tablet_v2*, const char*) {}
void tabletId(void*, zwp_tablet_v2*, uint32_t, uint32_t) {}
void tabletPath(void*, zwp_tablet_v2*, const char*) {}
void tabletDone(void*, zwp_tablet_v2*) {}
void tabletRemoved(void*, zwp_tablet_v2*) {}
void tabletBustype(void*, zwp_tablet_v2*, uint32_t) {}
void toolType(void* data, zwp_tablet_tool_v2*, uint32_t type) { tool(data).owner.onToolType(tool(data), type); }
void toolSerial(void*, zwp_tablet_tool_v2*, uint32_t, uint32_t) {}
void toolWacomId(void*, zwp_tablet_tool_v2*, uint32_t, uint32_t) {}
void toolCapability(void*, zwp_tablet_tool_v2*, uint32_t) {}
void toolDone(void*, zwp_tablet_tool_v2*) {}
void toolRemoved(void* data, zwp_tablet_tool_v2*) { tool(data).owner.onToolRemoved(tool(data)); }
void toolProximityIn(void* data, zwp_tablet_tool_v2*, uint32_t serial, zwp_tablet_v2*, wl_surface*) {
tool(data).owner.onProximityIn(tool(data), serial);
}
void toolProximityOut(void* data, zwp_tablet_tool_v2*) { tool(data).owner.onProximityOut(tool(data)); }
void toolDown(void* data, zwp_tablet_tool_v2*, uint32_t serial) {
tool(data).owner.recordSerial(serial);
tool(data).contactChange = 1;
}
void toolUp(void* data, zwp_tablet_tool_v2*) { tool(data).contactChange = -1; }
void toolMotion(void* data, zwp_tablet_tool_v2*, wl_fixed_t x, wl_fixed_t y) {
tool(data).position = { (halnf) wl_fixed_to_double(x), (halnf) wl_fixed_to_double(y) };
tool(data).moved = true;
}
void toolPressure(void* data, zwp_tablet_tool_v2*, uint32_t pressure) {
tool(data).sample.pressure = (halnf) pressure / 65535.0f;
}
void toolDistance(void* data, zwp_tablet_tool_v2*, uint32_t distance) {
tool(data).sample.distance = (halnf) distance / 65535.0f;
}
void toolTilt(void* data, zwp_tablet_tool_v2*, wl_fixed_t x, wl_fixed_t y) {
tool(data).sample.tilt = { (halnf) wl_fixed_to_double(x), (halnf) wl_fixed_to_double(y) };
}
void toolRotation(void* data, zwp_tablet_tool_v2*, wl_fixed_t degrees) {
tool(data).sample.rotation = (halnf) wl_fixed_to_double(degrees);
}
void toolSlider(void*, zwp_tablet_tool_v2*, int32_t) {}
void toolWheel(void*, zwp_tablet_tool_v2*, wl_fixed_t, int32_t) {}
void toolButton(void* data, zwp_tablet_tool_v2*, uint32_t serial, uint32_t button, uint32_t state) {
tool(data).owner.recordSerial(serial);
const bool pressed = state == ZWP_TABLET_TOOL_V2_BUTTON_STATE_PRESSED;
if (button == BTN_STYLUS) {
tool(data).linuxButtons.push_back({ BTN_RIGHT, pressed });
} else if (button == BTN_STYLUS2) {
tool(data).linuxButtons.push_back({ BTN_MIDDLE, pressed });
}
}
void toolFrame(void* data, zwp_tablet_tool_v2*, uint32_t time) { tool(data).owner.onFrame(tool(data), time); }
void padGroup(void*, zwp_tablet_pad_v2*, zwp_tablet_pad_group_v2*) {}
void padPath(void*, zwp_tablet_pad_v2*, const char*) {}
void padButtons(void*, zwp_tablet_pad_v2*, uint32_t) {}
void padDone(void*, zwp_tablet_pad_v2*) {}
void padButton(void*, zwp_tablet_pad_v2*, uint32_t, uint32_t, uint32_t) {}
void padEnter(void*, zwp_tablet_pad_v2*, uint32_t, zwp_tablet_v2*, wl_surface*) {}
void padLeave(void*, zwp_tablet_pad_v2*, uint32_t, wl_surface*) {}
void padRemoved(void*, zwp_tablet_pad_v2*) {}
const zwp_tablet_seat_v2_listener kTabletSeatListener = {
tabletAdded,
toolAdded,
padAdded,
};
const zwp_tablet_v2_listener kTabletListener = {
tabletName, tabletId, tabletPath, tabletDone, tabletRemoved, tabletBustype,
};
const zwp_tablet_tool_v2_listener kToolListener = {
toolType, toolSerial, toolWacomId, toolCapability, toolDone, toolRemoved, toolProximityIn,
toolProximityOut, toolDown, toolUp, toolMotion, toolPressure, toolDistance, toolTilt,
toolRotation, toolSlider, toolWheel, toolButton, toolFrame,
};
const zwp_tablet_pad_v2_listener kPadListener = {
padGroup, padPath, padButtons, padDone, padButton, padEnter, padLeave, padRemoved,
};
}
WaylandTablet::WaylandTablet(WaylandSeatContext& seat, zwp_tablet_manager_v2* manager, wl_seat* waylandSeat) :
mSeat(seat) {
mTabletSeat = zwp_tablet_manager_v2_get_tablet_seat(manager, waylandSeat);
zwp_tablet_seat_v2_add_listener(mTabletSeat, &kTabletSeatListener, this);
}
WaylandTablet::~WaylandTablet() {
for (const auto& tool : mTools) {
if (tool->shapeDevice) {
wp_cursor_shape_device_v1_destroy(tool->shapeDevice);
}
zwp_tablet_tool_v2_destroy(tool->handle);
}
for (zwp_tablet_pad_v2* pad : mPads) {
zwp_tablet_pad_v2_destroy(pad);
}
for (zwp_tablet_v2* tablet : mTablets) {
zwp_tablet_v2_destroy(tablet);
}
zwp_tablet_seat_v2_destroy(mTabletSeat);
}
void WaylandTablet::onTabletAdded(zwp_tablet_v2* tablet) {
mTablets.push_back(tablet);
zwp_tablet_v2_add_listener(tablet, &kTabletListener, this);
}
void WaylandTablet::onPadAdded(zwp_tablet_pad_v2* pad) {
mPads.push_back(pad);
zwp_tablet_pad_v2_add_listener(pad, &kPadListener, this);
}
void WaylandTablet::onToolAdded(zwp_tablet_tool_v2* handle) {
mTools.push_back(std::make_unique<Tool>(Tool{ *this, handle }));
Tool& tool = *mTools.back();
zwp_tablet_tool_v2_add_listener(handle, &kToolListener, &tool);
wp_cursor_shape_manager_v1* manager = mSeat.connection().cursorShapeManager();
if (manager) {
tool.shapeDevice = wp_cursor_shape_manager_v1_get_tablet_tool_v2(manager, handle);
}
}
void WaylandTablet::onToolRemoved(Tool& tool) {
if (tool.inside) {
onProximityOut(tool);
}
if (tool.shapeDevice) {
wp_cursor_shape_device_v1_destroy(tool.shapeDevice);
}
zwp_tablet_tool_v2_destroy(tool.handle);
std::erase_if(mTools, [&tool](const std::unique_ptr<Tool>& entry) { return entry.get() == &tool; });
}
void WaylandTablet::onToolType(Tool& tool, uint32_t type) {
tool.device = PointerDevice::Stylus;
tool.tip = StylusTip::Pen;
switch (type) {
case ZWP_TABLET_TOOL_V2_TYPE_ERASER: tool.tip = StylusTip::Eraser; break;
case ZWP_TABLET_TOOL_V2_TYPE_BRUSH: tool.tip = StylusTip::Brush; break;
case ZWP_TABLET_TOOL_V2_TYPE_PENCIL: tool.tip = StylusTip::Pencil; break;
case ZWP_TABLET_TOOL_V2_TYPE_AIRBRUSH: tool.tip = StylusTip::Airbrush; break;
case ZWP_TABLET_TOOL_V2_TYPE_FINGER: tool.device = PointerDevice::Finger; break;
case ZWP_TABLET_TOOL_V2_TYPE_MOUSE:
case ZWP_TABLET_TOOL_V2_TYPE_LENS: tool.device = PointerDevice::Mouse; break;
default: break;
}
tool.sample.device = tool.device;
tool.sample.tip = tool.tip;
}
void WaylandTablet::onProximityIn(Tool& tool, uint32_t serial) {
tool.inside = true;
tool.sample = PointerSample{ tool.device, tool.tip, 0 };
if (tool.shapeDevice) {
wp_cursor_shape_device_v1_set_shape(tool.shapeDevice, serial, WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR);
}
}
void WaylandTablet::onProximityOut(Tool& tool) {
tool.inside = false;
tool.contactChange = -1;
onFrame(tool, tool.sample.timeMs);
mSeat.surfacePointer().onPointerSurfaceLeft();
}
void WaylandTablet::onFrame(Tool& tool, uint32_t time) {
tool.sample.timeMs = time;
mSeat.input().post(PointerSampled{ tool.sample });
if (tool.moved && tool.inside) {
mSeat.surfacePointer().onPointerSurfaceMove(tool.position);
}
tool.moved = false;
if (tool.contactChange != 0) {
mSeat.surfacePointer().onPointerSurfaceButton(BTN_LEFT, tool.contactChange > 0, mSeat.lastInputSerial());
tool.contactChange = 0;
}
for (const auto& [linuxButton, pressed] : tool.linuxButtons) {
mSeat.surfacePointer().onPointerSurfaceButton(linuxButton, pressed, mSeat.lastInputSerial());
}
tool.linuxButtons.clear();
}
}

View file

@ -0,0 +1,60 @@
#pragma once
#include "WaylandSeatContext.hpp"
#include <memory>
#include <utility>
#include <vector>
struct wl_seat;
struct wp_cursor_shape_device_v1;
struct zwp_tablet_manager_v2;
struct zwp_tablet_seat_v2;
struct zwp_tablet_v2;
struct zwp_tablet_tool_v2;
struct zwp_tablet_pad_v2;
namespace tp {
// Tablet tools (pens, erasers, ...) reported as pointer samples, motion and buttons.
class WaylandTablet {
public:
WaylandTablet(WaylandSeatContext& seat, zwp_tablet_manager_v2* manager, wl_seat* waylandSeat);
~WaylandTablet();
WaylandTablet(const WaylandTablet&) = delete;
WaylandTablet& operator=(const WaylandTablet&) = delete;
struct Tool {
WaylandTablet& owner;
zwp_tablet_tool_v2* handle = nullptr;
wp_cursor_shape_device_v1* shapeDevice = nullptr;
PointerSample sample;
PointerDevice device = PointerDevice::Stylus;
StylusTip tip = StylusTip::Pen;
Vec2F position = { 0, 0 };
bool inside = false;
bool moved = false;
int contactChange = 0;
std::vector<std::pair<uint32_t, bool>> linuxButtons;
};
void onTabletAdded(zwp_tablet_v2* tablet);
void onToolAdded(zwp_tablet_tool_v2* tool);
void onPadAdded(zwp_tablet_pad_v2* pad);
void onToolType(Tool& tool, uint32_t type);
void onProximityIn(Tool& tool, uint32_t serial);
void onProximityOut(Tool& tool);
void onFrame(Tool& tool, uint32_t time);
void onToolRemoved(Tool& tool);
void recordSerial(uint32_t serial) { mSeat.recordSerial(serial); }
private:
WaylandSeatContext& mSeat;
zwp_tablet_seat_v2* mTabletSeat = nullptr;
std::vector<zwp_tablet_v2*> mTablets;
std::vector<zwp_tablet_pad_v2*> mPads;
std::vector<std::unique_ptr<Tool>> mTools;
};
}

View file

@ -0,0 +1,95 @@
#include "WaylandTextInput.hpp"
#include "text-input-unstable-v3-client-protocol.h"
namespace tp {
namespace {
WaylandTextInput& textInput(void* data) { return *static_cast<WaylandTextInput*>(data); }
void textEnter(void* data, zwp_text_input_v3*, wl_surface*) { textInput(data).onEnter(); }
void textLeave(void* data, zwp_text_input_v3*, wl_surface*) { textInput(data).onLeave(); }
void textPreedit(void*, zwp_text_input_v3*, const char*, int32_t, int32_t) {}
void textCommit(void* data, zwp_text_input_v3*, const char* text) { textInput(data).onCommitString(text); }
void textDeleteSurrounding(void*, zwp_text_input_v3*, uint32_t, uint32_t) {}
void textDone(void* data, zwp_text_input_v3*, uint32_t) { textInput(data).onDone(); }
const zwp_text_input_v3_listener kTextInputListener = {
textEnter, textLeave, textPreedit, textCommit, textDeleteSurrounding, textDone,
};
}
WaylandTextInput::WaylandTextInput(
WaylandSeatContext& seat, zwp_text_input_manager_v3* manager, wl_seat* waylandSeat
) :
mSeat(seat) {
mTextInput = zwp_text_input_manager_v3_get_text_input(manager, waylandSeat);
zwp_text_input_v3_add_listener(mTextInput, &kTextInputListener, this);
}
WaylandTextInput::~WaylandTextInput() {
if (mTextInput) {
zwp_text_input_v3_destroy(mTextInput);
}
}
void WaylandTextInput::setEnabled(bool enabled) {
if (mEnabled == enabled) {
return;
}
mEnabled = enabled;
sendState();
}
void WaylandTextInput::setCursorRect(const RectF& surfaceRect) {
if (mCursorRect == surfaceRect) {
return;
}
mCursorRect = surfaceRect;
if (mEnabled && mFocused) {
sendState();
}
}
void WaylandTextInput::onEnter() {
mFocused = true;
sendState();
}
void WaylandTextInput::onLeave() {
mFocused = false;
zwp_text_input_v3_disable(mTextInput);
zwp_text_input_v3_commit(mTextInput);
}
void WaylandTextInput::onCommitString(const char* text) { mPendingCommit = text ? text : ""; }
void WaylandTextInput::onDone() {
if (!mPendingCommit.empty()) {
mSeat.input().post(TextEntered{ mPendingCommit });
}
mPendingCommit.clear();
}
void WaylandTextInput::sendState() {
if (!mFocused) {
return;
}
if (mEnabled) {
zwp_text_input_v3_enable(mTextInput);
zwp_text_input_v3_set_content_type(
mTextInput, ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL
);
zwp_text_input_v3_set_cursor_rectangle(
mTextInput, (int32_t) mCursorRect.x, (int32_t) mCursorRect.y, (int32_t) mCursorRect.z, (int32_t) mCursorRect.w
);
} else {
zwp_text_input_v3_disable(mTextInput);
}
zwp_text_input_v3_commit(mTextInput);
}
}

View file

@ -0,0 +1,41 @@
#pragma once
#include "Rect.hpp"
#include "WaylandSeatContext.hpp"
#include <string>
struct wl_seat;
struct zwp_text_input_manager_v3;
struct zwp_text_input_v3;
namespace tp {
// Input-method text through zwp_text_input_v3.
class WaylandTextInput {
public:
WaylandTextInput(WaylandSeatContext& seat, zwp_text_input_manager_v3* manager, wl_seat* waylandSeat);
~WaylandTextInput();
WaylandTextInput(const WaylandTextInput&) = delete;
WaylandTextInput& operator=(const WaylandTextInput&) = delete;
void setEnabled(bool enabled);
void setCursorRect(const RectF& surfaceRect);
void onEnter();
void onLeave();
void onCommitString(const char* text);
void onDone();
private:
void sendState();
WaylandSeatContext& mSeat;
zwp_text_input_v3* mTextInput = nullptr;
bool mFocused = false;
bool mEnabled = false;
RectF mCursorRect = { 0, 0, 1, 1 };
std::string mPendingCommit;
};
}

View file

@ -0,0 +1,665 @@
#include "WaylandWindow.hpp"
#include <sys/eventfd.h>
#include "LinuxInputCodes.hpp"
#include "WaylandConnection.hpp"
#include "WaylandSeat.hpp"
#include "Timing.hpp"
#include "fractional-scale-v1-client-protocol.h"
#include "viewporter-client-protocol.h"
#include "xdg-activation-v1-client-protocol.h"
#include "xdg-decoration-unstable-v1-client-protocol.h"
#include "xdg-shell-client-protocol.h"
#include <vulkan/vulkan.h>
#include <vulkan/vulkan_wayland.h>
#include <wayland-client.h>
#include <linux/input-event-codes.h>
#include <poll.h>
#include <spawn.h>
#include <sys/wait.h>
#include <unistd.h>
#include <algorithm>
#include <cmath>
#include <filesystem>
#include <stdexcept>
#include <thread>
#include <vector>
namespace tp {
namespace {
constexpr halnf kFractionalScaleDenominator = 120;
constexpr halnf kMinimumContentWidth = 320;
constexpr halnf kMinimumContentHeight = 200;
constexpr int kConfigureWaitMs = 1000;
}
struct WaylandWindow::Objects {
wl_surface* surface = nullptr;
xdg_surface* shellSurface = nullptr;
xdg_toplevel* toplevel = nullptr;
wp_viewport* viewport = nullptr;
wp_fractional_scale_v1* fractionalScale = nullptr;
zxdg_toplevel_decoration_v1* decoration = nullptr;
WaylandConfigure pending;
wl_callback* frameCallback = nullptr;
};
struct WaylandListeners {
static WaylandWindow& window(void* data) { return *static_cast<WaylandWindow*>(data); }
static void surfaceConfigure(void* data, xdg_surface*, uint32_t serial) {
WaylandWindow& self = window(data);
WaylandConfigure configure = self.mObjects->pending;
configure.serial = serial;
self.mPendingConfigure = configure;
}
static void toplevelConfigure(void* data, xdg_toplevel*, int32_t width, int32_t height, wl_array* states) {
WaylandConfigure configure;
configure.width = width;
configure.height = height;
const auto* entries = static_cast<const uint32_t*>(states->data);
const size_t count = states->size / sizeof(uint32_t);
for (size_t index = 0; index < count; index++) {
switch (entries[index]) {
case XDG_TOPLEVEL_STATE_MAXIMIZED: configure.maximized = true; break;
case XDG_TOPLEVEL_STATE_FULLSCREEN: configure.fullscreen = true; break;
case XDG_TOPLEVEL_STATE_ACTIVATED: configure.activated = true; break;
case XDG_TOPLEVEL_STATE_TILED_LEFT:
case XDG_TOPLEVEL_STATE_TILED_RIGHT:
case XDG_TOPLEVEL_STATE_TILED_TOP:
case XDG_TOPLEVEL_STATE_TILED_BOTTOM: configure.tiled = true; break;
default: break;
}
}
window(data).mObjects->pending = configure;
}
static void toplevelClose(void* data, xdg_toplevel*) { window(data).mCloseRequested = true; }
static void toplevelBounds(void*, xdg_toplevel*, int32_t, int32_t) {}
static void toplevelCapabilities(void*, xdg_toplevel*, wl_array*) {}
static void preferredScale(void* data, wp_fractional_scale_v1*, uint32_t scale) {
window(data).applyScale((halnf) scale / kFractionalScaleDenominator);
}
static void decorationConfigure(void*, zxdg_toplevel_decoration_v1*, uint32_t) {}
static void frameDone(void* data, wl_callback* callback, uint32_t) {
WaylandWindow& self = window(data);
wl_callback_destroy(callback);
if (self.mObjects->frameCallback == callback) {
self.mObjects->frameCallback = nullptr;
}
}
};
namespace {
const xdg_surface_listener kShellSurfaceListener = { WaylandListeners::surfaceConfigure };
const xdg_toplevel_listener kToplevelListener = {
WaylandListeners::toplevelConfigure,
WaylandListeners::toplevelClose,
WaylandListeners::toplevelBounds,
WaylandListeners::toplevelCapabilities,
};
const wp_fractional_scale_v1_listener kFractionalScaleListener = { WaylandListeners::preferredScale };
const zxdg_toplevel_decoration_v1_listener kDecorationListener = { WaylandListeners::decorationConfigure };
const wl_callback_listener kFrameListener = { WaylandListeners::frameDone };
uint32_t resizeEdgeCode(WindowEdge edge) {
switch (edge) {
case WindowEdge::Top: return XDG_TOPLEVEL_RESIZE_EDGE_TOP;
case WindowEdge::Bottom: return XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM;
case WindowEdge::Left: return XDG_TOPLEVEL_RESIZE_EDGE_LEFT;
case WindowEdge::Right: return XDG_TOPLEVEL_RESIZE_EDGE_RIGHT;
case WindowEdge::TopLeft: return XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT;
case WindowEdge::TopRight: return XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT;
case WindowEdge::BottomLeft: return XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT;
case WindowEdge::BottomRight: return XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT;
case WindowEdge::None: return XDG_TOPLEVEL_RESIZE_EDGE_NONE;
}
return XDG_TOPLEVEL_RESIZE_EDGE_NONE;
}
uint32_t scaledLength(halnf length, halnf scale) { return (uint32_t) std::lround(length * scale); }
}
WaylandWindow::WaylandWindow(PlatformWindowHost& host, Vec2F contentSize, const WindowIdentity& identity) :
mHost(host),
mConnection(std::make_unique<WaylandConnection>()),
mObjects(std::make_unique<Objects>()),
mWakeDescriptor(eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK)) {
mFrame.setContentSize(contentSize);
mRestoredContentSize = contentSize;
WaylandConnection& connection = *mConnection;
Objects& objects = *mObjects;
objects.surface = wl_compositor_create_surface(connection.compositor());
objects.shellSurface = xdg_wm_base_get_xdg_surface(connection.windowManager(), objects.surface);
xdg_surface_add_listener(objects.shellSurface, &kShellSurfaceListener, this);
objects.toplevel = xdg_surface_get_toplevel(objects.shellSurface);
xdg_toplevel_add_listener(objects.toplevel, &kToplevelListener, this);
xdg_toplevel_set_title(objects.toplevel, identity.title.c_str());
xdg_toplevel_set_app_id(objects.toplevel, identity.appId.c_str());
xdg_toplevel_set_min_size(objects.toplevel, (int32_t) kMinimumContentWidth, (int32_t) kMinimumContentHeight);
if (connection.decorationManager()) {
objects.decoration =
zxdg_decoration_manager_v1_get_toplevel_decoration(connection.decorationManager(), objects.toplevel);
zxdg_toplevel_decoration_v1_add_listener(objects.decoration, &kDecorationListener, this);
zxdg_toplevel_decoration_v1_set_mode(objects.decoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE);
}
if (connection.viewporter()) {
objects.viewport = wp_viewporter_get_viewport(connection.viewporter(), objects.surface);
}
if (connection.fractionalScaleManager() && objects.viewport) {
objects.fractionalScale =
wp_fractional_scale_manager_v1_get_fractional_scale(connection.fractionalScaleManager(), objects.surface);
wp_fractional_scale_v1_add_listener(objects.fractionalScale, &kFractionalScaleListener, this);
}
mSeat = std::make_unique<WaylandSeat>(connection, mHost, *this);
updateFrameGeometry();
wl_surface_commit(objects.surface);
connection.flush();
}
WaylandWindow::~WaylandWindow() {
if (mWakeDescriptor >= 0) {
close(mWakeDescriptor);
}
mSeat.reset();
Objects& objects = *mObjects;
if (objects.frameCallback) {
wl_callback_destroy(objects.frameCallback);
}
if (objects.decoration) {
zxdg_toplevel_decoration_v1_destroy(objects.decoration);
}
if (objects.fractionalScale) {
wp_fractional_scale_v1_destroy(objects.fractionalScale);
}
if (objects.viewport) {
wp_viewport_destroy(objects.viewport);
}
if (objects.toplevel) {
xdg_toplevel_destroy(objects.toplevel);
}
if (objects.shellSurface) {
xdg_surface_destroy(objects.shellSurface);
}
if (objects.surface) {
wl_surface_destroy(objects.surface);
}
mConnection->flush();
}
VkSurfaceKHR WaylandWindow::createSurface(VkInstance instance) const {
VkWaylandSurfaceCreateInfoKHR createInfo{
.sType = VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR,
.display = mConnection->display(),
.surface = mObjects->surface,
};
VkSurfaceKHR surface = VK_NULL_HANDLE;
if (vkCreateWaylandSurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) {
return VK_NULL_HANDLE;
}
return surface;
}
VkExtent2D WaylandWindow::framebufferExtent() const {
const Vec2F surface = mFrame.surfaceSize();
return { scaledLength(surface.x, mScale), scaledLength(surface.y, mScale) };
}
void WaylandWindow::onTranslucencyResolved(bool translucent) {
mFrame.setTranslucent(translucent);
mGeometryDirty = true;
}
void WaylandWindow::dispatchEvents(halnf timeoutMs) {
const bool waitsForever = timeoutMs < 0;
const time_ns deadline = get_time_ns() + (time_ns) (std::max(timeoutMs, 0.0f) * 1.0e6f);
while (true) {
const halnf remainingMs = waitsForever ? -1.0f : (halnf) ((double) (deadline - get_time_ns()) / 1.0e6);
if (pollOnce(waitsForever ? -1 : std::max(0, (int) std::ceil(remainingMs)))) {
return;
}
if (!waitsForever && get_time_ns() >= deadline) {
return;
}
}
}
void WaylandWindow::wake() {
if (mWakeDescriptor >= 0) {
eventfd_write(mWakeDescriptor, 1);
}
}
bool WaylandWindow::pollOnce(int timeoutMs) {
wl_display* display = mConnection->display();
mConnection->flush();
if (wl_display_dispatch_pending(display) > 0) {
return true;
}
while (wl_display_prepare_read(display) != 0) {
if (wl_display_dispatch_pending(display) > 0) {
return true;
}
}
mConnection->flush();
std::vector<pollfd> descriptors = {
{ wl_display_get_fd(display), POLLIN, 0 },
{ mWakeDescriptor, POLLIN, 0 },
};
mSeat->collectPollDescriptors(descriptors);
const int ready = poll(descriptors.data(), (nfds_t) descriptors.size(), timeoutMs);
if (ready > 0 && (descriptors[0].revents & POLLIN)) {
if (wl_display_read_events(display) < 0) {
throw std::runtime_error("lost the Wayland connection");
}
} else {
wl_display_cancel_read(display);
}
bool handled = wl_display_dispatch_pending(display) > 0;
if (ready > 0 && (descriptors[1].revents & POLLIN)) {
eventfd_t ignored = 0;
eventfd_read(mWakeDescriptor, &ignored);
handled = true;
}
if (ready > 0) {
const std::vector<pollfd> seatDescriptors(descriptors.begin() + 2, descriptors.end());
mSeat->onDescriptorsReady(seatDescriptors);
handled = handled || std::any_of(seatDescriptors.begin(), seatDescriptors.end(), [](const pollfd& entry) {
return entry.revents != 0;
});
}
mConnection->flush();
return handled;
}
void WaylandWindow::waitUntilConfigured() {
while (!mConfigured) {
if (!mConnection->dispatch(kConfigureWaitMs)) {
break;
}
applyPendingConfigure();
}
}
void WaylandWindow::applyPendingConfigure() {
if (!mPendingConfigure) {
return;
}
const WaylandConfigure configure = *mPendingConfigure;
mPendingConfigure.reset();
applyConfigure(configure);
xdg_surface_ack_configure(mObjects->shellSurface, configure.serial);
mConfigured = true;
notifyOwnerResized();
}
void WaylandWindow::applyConfigure(const WaylandConfigure& configure) {
mMaximized = configure.maximized || configure.fullscreen;
mFullScreen = configure.fullscreen;
mActivated = configure.activated;
const bool tiled = mMaximized || configure.tiled;
if (tiled != mFrame.isTiled()) {
mFrame.setTiled(tiled);
mGeometryDirty = true;
}
Vec2F contentSize = mFrame.contentSize();
if (configure.width > 0 && configure.height > 0) {
contentSize = { (halnf) configure.width, (halnf) configure.height };
} else if (!tiled) {
contentSize = mRestoredContentSize;
}
contentSize.x = std::max(contentSize.x, kMinimumContentWidth);
contentSize.y = std::max(contentSize.y, kMinimumContentHeight);
if (!tiled) {
mRestoredContentSize = contentSize;
}
if (contentSize != mFrame.contentSize()) {
mFrame.setContentSize(contentSize);
mGeometryDirty = true;
}
}
void WaylandWindow::applyScale(halnf scale) {
if (scale <= 0 || scale == mScale) {
return;
}
mScale = scale;
mGeometryDirty = true;
if (mConfigured) {
notifyOwnerResized();
}
}
void WaylandWindow::notifyOwnerResized() { mHost.onContentResized(); }
void WaylandWindow::setAcceptsInput(bool accepts) {
mAcceptsInput = accepts;
mGeometryDirty = true;
updateFrameGeometry();
wl_surface_commit(mObjects->surface);
mConnection->flush();
}
void WaylandWindow::commitFrameGeometry() {
if (mGeometryDirty) {
updateFrameGeometry();
}
requestFrameCallback();
}
void WaylandWindow::requestFrameCallback() {
if (mObjects->frameCallback) {
return;
}
mObjects->frameCallback = wl_surface_frame(mObjects->surface);
wl_callback_add_listener(mObjects->frameCallback, &kFrameListener, this);
}
bool WaylandWindow::isWaitingForFrame() const { return mObjects->frameCallback != nullptr; }
void WaylandWindow::updateFrameGeometry() {
Objects& objects = *mObjects;
const Vec2F surface = mFrame.surfaceSize();
const RectF content = mFrame.contentRect();
const RectF input = mFrame.inputRect();
xdg_surface_set_window_geometry(
objects.shellSurface, (int32_t) content.x, (int32_t) content.y, (int32_t) content.z, (int32_t) content.w
);
wl_region* inputRegion = wl_compositor_create_region(mConnection->compositor());
if (mAcceptsInput) {
wl_region_add(inputRegion, (int32_t) input.x, (int32_t) input.y, (int32_t) input.z, (int32_t) input.w);
}
wl_surface_set_input_region(objects.surface, inputRegion);
wl_region_destroy(inputRegion);
if (!mFrame.hasShadow()) {
wl_region* opaqueRegion = wl_compositor_create_region(mConnection->compositor());
wl_region_add(opaqueRegion, 0, 0, (int32_t) surface.x, (int32_t) surface.y);
wl_surface_set_opaque_region(objects.surface, opaqueRegion);
wl_region_destroy(opaqueRegion);
} else {
wl_surface_set_opaque_region(objects.surface, nullptr);
}
if (objects.viewport) {
wp_viewport_set_destination(objects.viewport, (int32_t) surface.x, (int32_t) surface.y);
} else {
wl_surface_set_buffer_scale(objects.surface, (int32_t) std::lround(mScale));
}
mGeometryDirty = false;
}
namespace {
struct ActivationToken {
std::string value;
bool done = false;
};
void activationTokenDone(void* data, xdg_activation_token_v1*, const char* token) {
auto& result = *static_cast<ActivationToken*>(data);
result.value = token ? token : "";
result.done = true;
}
const xdg_activation_token_v1_listener kActivationTokenListener = { activationTokenDone };
}
std::string WaylandWindow::activationToken() {
xdg_activation_v1* activation = mConnection->activation();
if (!activation) {
return {};
}
wl_display* display = mConnection->display();
wl_event_queue* queue = wl_display_create_queue(display);
auto* wrapped = static_cast<xdg_activation_v1*>(wl_proxy_create_wrapper(activation));
wl_proxy_set_queue(reinterpret_cast<wl_proxy*>(wrapped), queue);
ActivationToken result;
xdg_activation_token_v1* token = xdg_activation_v1_get_activation_token(wrapped);
xdg_activation_token_v1_add_listener(token, &kActivationTokenListener, &result);
if (mConnection->seat() && mSeat->lastInputSerial() != 0) {
xdg_activation_token_v1_set_serial(token, mSeat->lastInputSerial(), mConnection->seat());
}
xdg_activation_token_v1_set_surface(token, mObjects->surface);
xdg_activation_token_v1_commit(token);
while (!result.done && wl_display_roundtrip_queue(display, queue) >= 0) {
}
xdg_activation_token_v1_destroy(token);
wl_proxy_wrapper_destroy(wrapped);
wl_event_queue_destroy(queue);
return result.value;
}
bool WaylandWindow::openUrl(const std::string& url) {
if (url.empty()) {
return false;
}
std::vector<std::string> environment;
for (char** entry = environ; *entry; entry++) {
const std::string variable = *entry;
if (variable.starts_with("XDG_ACTIVATION_TOKEN=") || variable.starts_with("DESKTOP_STARTUP_ID=")) {
continue;
}
environment.push_back(variable);
}
const std::string token = activationToken();
if (!token.empty()) {
environment.push_back("XDG_ACTIVATION_TOKEN=" + token);
environment.push_back("DESKTOP_STARTUP_ID=" + token);
}
std::vector<char*> environmentPointers;
for (std::string& variable : environment) {
environmentPointers.push_back(variable.data());
}
environmentPointers.push_back(nullptr);
std::string program = "xdg-open";
std::string argument = url;
char* arguments[] = { program.data(), argument.data(), nullptr };
pid_t child = 0;
if (posix_spawnp(&child, program.c_str(), nullptr, nullptr, arguments, environmentPointers.data()) != 0) {
return false;
}
std::thread([child]() { waitpid(child, nullptr, 0); }).detach();
return true;
}
Vec2F WaylandWindow::toContent(Vec2F surfacePoint) const {
const RectF content = mFrame.contentRect();
return { surfacePoint.x - content.x, surfacePoint.y - content.y };
}
void WaylandWindow::onPointerSurfaceMove(Vec2F surfacePoint) {
const bool contentHoldsPointer = mHeldButtons > 0;
const WindowEdge edge = contentHoldsPointer ? WindowEdge::None : mFrame.edgeAt(surfacePoint);
mHoveredEdge = edge;
if (edge != WindowEdge::None) {
mSeat->setCursor(cursorForEdge(edge));
if (mPointerOnContent) {
mPointerOnContent = false;
input().post(PointerLeft{});
}
return;
}
mSeat->setCursor(mClientCursor);
mPointerOnContent = true;
input().post(PointerMoved{ toContent(surfacePoint) });
}
// Touchpad pinches arrive at the pointer; the gesture is placed where the pointer is, in content coordinates.
void WaylandWindow::onPointerSurfacePinch(const TouchGesture& gesture) {
TouchGesture content = gesture;
content.center = toContent(gesture.center);
input().post(GestureChanged{ content });
}
void WaylandWindow::onPointerSurfaceLeft() {
mHeldButtons = 0;
mHoveredEdge = WindowEdge::None;
mPointerOnContent = false;
input().post(PointerLeft{});
}
void WaylandWindow::onPointerSurfaceButton(uint32_t linuxButton, bool pressed, uint32_t serial) {
if (pressed && mHoveredEdge != WindowEdge::None) {
if (linuxButton == BTN_LEFT) {
xdg_toplevel_resize(mObjects->toplevel, mConnection->seat(), serial, resizeEdgeCode(mHoveredEdge));
}
return;
}
const std::optional<PointerButton> button = pointerButtonFromLinux(linuxButton);
if (!button) {
return;
}
if (pressed) {
mHeldButtons++;
} else if (mHeldButtons > 0) {
mHeldButtons--;
}
input().post(PointerButtonChanged{ *button, pressed });
}
void WaylandWindow::minimize() { xdg_toplevel_set_minimized(mObjects->toplevel); }
void WaylandWindow::toggleMaximize() {
if (mMaximized) {
xdg_toplevel_unset_maximized(mObjects->toplevel);
} else {
xdg_toplevel_set_maximized(mObjects->toplevel);
}
}
void WaylandWindow::setFullScreen(bool fullScreen) {
if (fullScreen) {
xdg_toplevel_set_fullscreen(mObjects->toplevel, nullptr);
} else {
xdg_toplevel_unset_fullscreen(mObjects->toplevel);
}
}
void WaylandWindow::beginMove() {
xdg_toplevel_move(mObjects->toplevel, mConnection->seat(), mSeat->lastInputSerial());
}
void WaylandWindow::beginResize(WindowEdge edge) {
if (edge == WindowEdge::None) {
return;
}
xdg_toplevel_resize(mObjects->toplevel, mConnection->seat(), mSeat->lastInputSerial(), resizeEdgeCode(edge));
}
void WaylandWindow::showWindowMenu(Vec2F contentPosition) {
const RectF content = mFrame.contentRect();
xdg_toplevel_show_window_menu(
mObjects->toplevel,
mConnection->seat(),
mSeat->lastInputSerial(),
(int32_t) (contentPosition.x + content.x),
(int32_t) (contentPosition.y + content.y)
);
}
void WaylandWindow::setCursor(CursorShape shape) {
mClientCursor = shape;
if (mHoveredEdge == WindowEdge::None) {
mSeat->setCursor(shape);
}
}
void WaylandWindow::setClipboardText(const std::string& text) { mSeat->setClipboardText(text); }
std::string WaylandWindow::clipboardText() { return mSeat->clipboardText(); }
ClipboardImage WaylandWindow::clipboardImage() { return mSeat->clipboardImage(); }
void WaylandWindow::setTextInputEnabled(bool enabled) { mSeat->setTextInputEnabled(enabled); }
void WaylandWindow::setTextInputRect(const RectF& contentRect) {
const RectF content = mFrame.contentRect();
mSeat->setTextInputRect({ contentRect.x + content.x, contentRect.y + content.y, contentRect.z, contentRect.w });
}
PlatformInfo WaylandWindow::platformInfo() const {
PlatformInfo info;
info.system = OperatingSystem::Linux;
info.formFactor = FormFactor::Desktop;
info.contentScale = mScale;
info.screenSize = mFrame.contentSize();
info.hasMouse = mSeat->hasPointer();
info.hasTouch = mSeat->hasTouch();
info.hasPen = mSeat->hasTablet();
info.hasPhysicalKeyboard = mSeat->hasKeyboard();
info.hasHoverPointer = info.hasMouse || info.hasPen;
return info;
}
std::vector<const char*> platformSurfaceExtensions() {
return { VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME };
}
// Resources are installed next to the executable, so a moved or installed binary still finds them.
std::filesystem::path platformResourceDirectory() {
std::error_code error;
const std::filesystem::path executable = std::filesystem::read_symlink("/proc/self/exe", error);
if (error) {
return "rsc";
}
return executable.parent_path() / "rsc";
}
std::unique_ptr<PlatformWindow> createPlatformWindow(
PlatformWindowHost& host, Vec2F contentSize, const WindowIdentity& identity, const PlatformContext&
) {
return std::make_unique<WaylandWindow>(host, contentSize, identity);
}
}

View file

@ -0,0 +1,122 @@
#pragma once
#include "DesktopWindowControls.hpp"
#include "PlatformWindow.hpp"
#include "WaylandSeatContext.hpp"
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
struct wl_display;
namespace tp {
class WaylandConnection;
class WaylandSeat;
// One xdg_toplevel's pending configure; applied and acknowledged by the frame loop.
struct WaylandConfigure {
int32_t width = 0;
int32_t height = 0;
bool maximized = false;
bool fullscreen = false;
bool activated = false;
bool tiled = false;
uint32_t serial = 0;
};
class WaylandWindow final : public PlatformWindow, public DesktopWindowControls, public WaylandSurfacePointer {
public:
WaylandWindow(PlatformWindowHost& host, Vec2F contentSize, const WindowIdentity& identity);
~WaylandWindow() override;
WaylandWindow(const WaylandWindow&) = delete;
WaylandWindow& operator=(const WaylandWindow&) = delete;
[[nodiscard]] VkSurfaceKHR createSurface(VkInstance instance) const override;
[[nodiscard]] VkExtent2D framebufferExtent() const override;
[[nodiscard]] bool wantsTranslucency() const override { return true; }
void setAcceptsInput(bool accepts) override;
void onTranslucencyResolved(bool translucent) override;
void dispatchEvents(halnf timeoutMs) override;
void wake() override;
void waitUntilConfigured() override;
[[nodiscard]] bool hasPendingConfigure() const override { return mPendingConfigure.has_value(); }
void applyPendingConfigure() override;
void commitFrameGeometry() override;
[[nodiscard]] bool isWaitingForFrame() const override;
[[nodiscard]] bool shouldClose() const override { return mCloseRequested; }
void requestClose() override { mCloseRequested = true; }
[[nodiscard]] const WindowFrame& frame() const override { return mFrame; }
[[nodiscard]] halnf scale() const override { return mScale; }
[[nodiscard]] bool isMaximized() const override { return mMaximized; }
[[nodiscard]] bool isFocused() const override { return mActivated; }
void minimize() override;
[[nodiscard]] DesktopWindowControls* desktopControls() override { return this; }
void toggleMaximize() override;
void setFullScreen(bool fullScreen) override;
[[nodiscard]] bool isFullScreen() const override { return mFullScreen; }
void beginMove() override;
void beginResize(WindowEdge edge) override;
void showWindowMenu(Vec2F contentPosition) override;
void setCursor(CursorShape shape) override;
void setClipboardText(const std::string& text) override;
[[nodiscard]] std::string clipboardText() override;
[[nodiscard]] ClipboardImage clipboardImage() override;
bool openUrl(const std::string& url) override;
void setTextInputEnabled(bool enabled) override;
void setTextInputRect(const RectF& contentRect) override;
[[nodiscard]] PlatformInfo platformInfo() const override;
private:
friend struct WaylandListeners;
void applyConfigure(const WaylandConfigure& configure);
void applyScale(halnf scale);
void notifyOwnerResized();
[[nodiscard]] bool pollOnce(int timeoutMs);
void updateFrameGeometry();
void requestFrameCallback();
void onPointerSurfaceMove(Vec2F surfacePoint) override;
void onPointerSurfaceButton(uint32_t linuxButton, bool pressed, uint32_t serial) override;
void onPointerSurfaceLeft() override;
void onPointerSurfacePinch(const TouchGesture& gesture) override;
[[nodiscard]] InputSink& input() { return mHost.rawInput(); }
[[nodiscard]] Vec2F toContent(Vec2F surfacePoint) const;
[[nodiscard]] std::string activationToken();
PlatformWindowHost& mHost;
bool mAcceptsInput = true;
bool mPointerOnContent = false;
std::unique_ptr<WaylandConnection> mConnection;
std::unique_ptr<WaylandSeat> mSeat;
int mWakeDescriptor = -1;
struct Objects;
std::unique_ptr<Objects> mObjects;
WindowFrame mFrame;
Vec2F mRestoredContentSize;
halnf mScale = 1;
bool mConfigured = false;
std::optional<WaylandConfigure> mPendingConfigure;
bool mCloseRequested = false;
bool mMaximized = false;
bool mFullScreen = false;
bool mActivated = false;
bool mGeometryDirty = true;
WindowEdge mHoveredEdge = WindowEdge::None;
int mHeldButtons = 0;
CursorShape mClientCursor = CursorShape::Default;
};
}

View file

@ -0,0 +1,433 @@
#include "CanvasContext.hpp"
#include "GpuError.hpp"
#include "Profiler.hpp"
#include <nanovg_vk.h>
#include <vector>
using namespace tp;
namespace {
constexpr halnf kItalicSkew = -0.21f;
bool isItalic(FontFace face) { return face == FontFace::Italic || face == FontFace::BoldItalic; }
void addRect(NVGcontext* vg, const RectF& rect, halnf round) {
if (round == 0) {
nvgRect(vg, rect.x, rect.y, rect.z, rect.w);
} else {
nvgRoundedRect(vg, rect.x, rect.y, rect.z, rect.w, round);
}
}
void stroke(NVGcontext* vg, const RGBA& color, halnf width) {
nvgStrokeWidth(vg, width);
nvgStrokeColor(vg, toNVGColor(color));
nvgStroke(vg);
}
void fill(NVGcontext* vg, const RGBA& color) {
nvgFillColor(vg, toNVGColor(color));
nvgFill(vg);
}
NVGpaint linearPaint(NVGcontext* vg, const Canvas::LinearGradient& gradient, Vec2F origin) {
const Vec2F from = gradient.from + origin;
const Vec2F to = gradient.to + origin;
return nvgLinearGradient(
vg, from.x, from.y, to.x, to.y, toNVGColor(gradient.fromColor), toNVGColor(gradient.toColor)
);
}
int nvgCompositeOperationOf(Canvas::CompositeOperation operation) {
switch (operation) {
case Canvas::CompositeOperation::SourceOver: return NVG_SOURCE_OVER;
case Canvas::CompositeOperation::DestinationOut: return NVG_DESTINATION_OUT;
case Canvas::CompositeOperation::DestinationOver: return NVG_DESTINATION_OVER;
}
return NVG_SOURCE_OVER;
}
int nvgWindingOf(Canvas::PathWinding winding) { return winding == Canvas::PathWinding::Hole ? NVG_HOLE : NVG_SOLID; }
}
Canvas::Canvas(WindowSurface& surface, Profiler& profiler, const std::filesystem::path& resourceDirectory) :
mContext(std::make_unique<Context>()) {
mContext->surface = &surface;
mContext->profiler = &profiler;
mContext->flushSection = profiler.section("frame.ui_flush");
const GpuDevice& gpu = surface.gpu();
VKNVGCreateInfo createInfo{};
createInfo.gpu = gpu.physicalDevice();
createInfo.device = gpu.device();
createInfo.renderpass = surface.renderPass();
createInfo.cmdBuffer = mContext->frameCommandBuffers;
createInfo.frameCount = WindowSurface::kFramesInFlight;
createInfo.currentFrame = &mContext->frameSlot;
createInfo.commandPool = gpu.commandPool();
createInfo.graphicsQueueFamilyIndex = gpu.graphicsQueueFamily();
mContext->vg = nvgCreateVk(&createInfo, NVG_ANTIALIAS | NVG_STENCIL_STROKES, gpu.graphicsQueue());
if (!mContext->vg) {
throw GpuError("creating the NanoVG context", VK_ERROR_INITIALIZATION_FAILED);
}
mContext->fonts = loadCanvasFonts(mContext->vg, resourceDirectory);
}
Canvas::~Canvas() {
mContext->surface->gpu().waitIdle();
nvgDeleteVk(mContext->vg);
}
void Canvas::drawBegin(const GpuFrame& frame, const WindowFrame& windowFrame) {
mContext->frameCommandBuffers[frame.slot] = frame.commandBuffer;
mContext->frameSlot = frame.slot;
const Vec2F surfaceSize = windowFrame.surfaceSize();
const halnf devicePixelRatio = surfaceSize.x > 0 ? (halnf) frame.extent.width / surfaceSize.x : 1.0f;
nvgBeginFrame(mContext->vg, surfaceSize.x, surfaceSize.y, devicePixelRatio);
nvgTranslate(mContext->vg, windowFrame.margin(), windowFrame.margin());
mOrigin = { 0, 0 };
mClips.clear();
mSavedFrames.clear();
}
void Canvas::drawEnd() {
const ProfileScope scope(*mContext->profiler, mContext->flushSection);
nvgEndFrame(mContext->vg);
}
void Canvas::save() {
nvgSave(mContext->vg);
mSavedFrames.append({ mOrigin, mClips.size() });
}
void Canvas::restore() {
DEBUG_ASSERT(mSavedFrames.size());
const SavedFrame saved = mSavedFrames.last();
mSavedFrames.pop();
while (mClips.size() > saved.clipDepth) {
mClips.pop();
}
mOrigin = saved.origin;
nvgRestore(mContext->vg);
applyClip();
}
void Canvas::setCompositeOperation(CompositeOperation operation) {
nvgGlobalCompositeOperation(mContext->vg, nvgCompositeOperationOf(operation));
}
void Canvas::beginPath() { nvgBeginPath(mContext->vg); }
void Canvas::addPathRect(RectF rect, halnf round) {
rect.pos += mOrigin;
addRect(mContext->vg, rect, round);
}
void Canvas::setPathWinding(PathWinding winding) { nvgPathWinding(mContext->vg, nvgWindingOf(winding)); }
void Canvas::fillPath(const RGBA& color) { fill(mContext->vg, color); }
void Canvas::fillPath(const BoxGradient& gradient) {
const RectF box = { gradient.box.pos + mOrigin, gradient.box.size };
const NVGpaint paint = nvgBoxGradient(
mContext->vg,
box.x,
box.y,
box.z,
box.w,
gradient.radius,
gradient.feather,
toNVGColor(gradient.innerColor),
toNVGColor(gradient.outerColor)
);
nvgFillPaint(mContext->vg, paint);
nvgFill(mContext->vg);
}
void Canvas::rect(RectF rect, const RGBA& color, halnf round) {
rect.pos += mOrigin;
nvgBeginPath(mContext->vg);
addRect(mContext->vg, rect, round);
fill(mContext->vg, color);
}
void Canvas::gradientRect(RectF rect, const RGBA& from, const RGBA& to, bool horizontal, halnf round) {
rect.pos += mOrigin;
NVGcontext* vg = mContext->vg;
const halnf endX = horizontal ? rect.x + rect.z : rect.x;
const halnf endY = horizontal ? rect.y : rect.y + rect.w;
const NVGpaint paint = nvgLinearGradient(vg, rect.x, rect.y, endX, endY, toNVGColor(from), toNVGColor(to));
nvgBeginPath(vg);
addRect(vg, rect, round);
nvgFillPaint(vg, paint);
nvgFill(vg);
}
void Canvas::debugCross(RectF rect, const RGBA& color) {
rect.pos += mOrigin;
NVGcontext* vg = mContext->vg;
nvgBeginPath(vg);
nvgMoveTo(vg, rect.p1().x, rect.p1().y);
nvgLineTo(vg, rect.p3().x, rect.p3().y);
nvgMoveTo(vg, rect.p2().x, rect.p2().y);
nvgLineTo(vg, rect.p4().x, rect.p4().y);
stroke(vg, color, 2);
}
void Canvas::frame(RectF rect, const RGBA& color) {
rect.pos += mOrigin;
NVGcontext* vg = mContext->vg;
nvgBeginPath(vg);
nvgRect(vg, rect.x, rect.y, rect.z, rect.w);
stroke(vg, color, 2);
}
void Canvas::outline(RectF rect, const RGBA& color, halnf round, halnf width) {
rect.pos += mOrigin;
const halnf inset = width / 2;
nvgBeginPath(mContext->vg);
nvgRoundedRect(
mContext->vg, rect.x + inset, rect.y + inset, rect.z - width, rect.w - width, round > inset ? round - inset : 0
);
stroke(mContext->vg, color, width);
}
void Canvas::circleOutline(Vec2F center, halnf radius, const RGBA& color, halnf width) {
center += mOrigin;
nvgBeginPath(mContext->vg);
nvgCircle(mContext->vg, center.x, center.y, radius - width / 2);
stroke(mContext->vg, color, width);
}
void Canvas::polyline(const Vec2F* points, int count, const RGBA& color, halnf width) {
if (count < 2) {
return;
}
nvgBeginPath(mContext->vg);
nvgMoveTo(mContext->vg, points[0].x + mOrigin.x, points[0].y + mOrigin.y);
for (int index = 1; index < count; index++) {
nvgLineTo(mContext->vg, points[index].x + mOrigin.x, points[index].y + mOrigin.y);
}
nvgLineCap(mContext->vg, NVG_ROUND);
nvgLineJoin(mContext->vg, NVG_ROUND);
stroke(mContext->vg, color, width);
}
void Canvas::polygon(const Vec2F* points, int count, const RGBA& color) {
if (count < 3) {
return;
}
nvgBeginPath(mContext->vg);
nvgMoveTo(mContext->vg, points[0].x + mOrigin.x, points[0].y + mOrigin.y);
for (int index = 1; index < count; index++) {
nvgLineTo(mContext->vg, points[index].x + mOrigin.x, points[index].y + mOrigin.y);
}
nvgClosePath(mContext->vg);
fill(mContext->vg, color);
}
void Canvas::polygon(const Vec2F* points, int count, const LinearGradient& gradient) {
if (count < 3) {
return;
}
nvgBeginPath(mContext->vg);
nvgMoveTo(mContext->vg, points[0].x + mOrigin.x, points[0].y + mOrigin.y);
for (int index = 1; index < count; index++) {
nvgLineTo(mContext->vg, points[index].x + mOrigin.x, points[index].y + mOrigin.y);
}
nvgClosePath(mContext->vg);
nvgFillPaint(mContext->vg, linearPaint(mContext->vg, gradient, mOrigin));
nvgFill(mContext->vg);
}
void Canvas::ringSegment(
Vec2F center, halnf innerRadius, halnf outerRadius, halnf fromAngle, halnf toAngle, const LinearGradient& gradient
) {
center += mOrigin;
nvgBeginPath(mContext->vg);
nvgArc(mContext->vg, center.x, center.y, innerRadius, fromAngle, toAngle, NVG_CW);
nvgArc(mContext->vg, center.x, center.y, outerRadius, toAngle, fromAngle, NVG_CCW);
nvgClosePath(mContext->vg);
nvgFillPaint(mContext->vg, linearPaint(mContext->vg, gradient, mOrigin));
nvgFill(mContext->vg);
}
void Canvas::radialGlow(
Vec2F center, halnf innerRadius, halnf outerRadius, const RGBA& innerColor, const RGBA& outerColor
) {
center += mOrigin;
const NVGpaint paint = nvgRadialGradient(
mContext->vg, center.x, center.y, innerRadius, outerRadius, toNVGColor(innerColor), toNVGColor(outerColor)
);
nvgBeginPath(mContext->vg);
nvgCircle(mContext->vg, center.x, center.y, outerRadius);
nvgFillPaint(mContext->vg, paint);
nvgFill(mContext->vg);
}
void Canvas::line(Vec2F start, Vec2F end, const RGBA& color, halnf thickness) {
start += mOrigin;
end += mOrigin;
nvgBeginPath(mContext->vg);
nvgMoveTo(mContext->vg, start.x, start.y);
nvgLineTo(mContext->vg, end.x, end.y);
stroke(mContext->vg, color, thickness);
}
void Canvas::circle(Vec2F center, halnf radius, const RGBA& color) {
center += mOrigin;
nvgBeginPath(mContext->vg);
nvgCircle(mContext->vg, center.x, center.y, radius);
fill(mContext->vg, color);
}
void Canvas::clipTo(const RectF& rect) {
const RectF placed = { rect.pos + mOrigin, rect.size };
RectF clip = placed;
if (mClips.size()) {
mClips.last().calcIntersection(placed, clip);
}
mClips.append(clip);
applyClip();
}
void Canvas::applyClip() {
if (!mClips.size()) {
nvgResetScissor(mContext->vg);
return;
}
const RectF& clip = mClips.last();
nvgScissor(mContext->vg, clip.x, clip.y, clip.z, clip.w);
}
bool Canvas::isClippedAway() const { return mClips.size() && mClips.last().size.length2() <= EPSILON; }
bool Canvas::isOutsideClip(const RectF& rect) const {
if (!mClips.size()) {
return false;
}
const RectF placed = { rect.pos + mOrigin, rect.size };
const RectF& clip = mClips.last();
return placed.x >= clip.x + clip.z || clip.x >= placed.x + placed.z || placed.y >= clip.y + clip.w ||
clip.y >= placed.y + placed.w;
}
void Canvas::text(const char* string, const RectF& area, const TextStyle& style) {
const halnf padding = style.padding;
const RectF padded = { area.x + padding, area.y + padding, area.z - padding * 2, area.w - padding * 2 };
if (isOutsideClip(padded)) {
return;
}
const SavedState saved(*this);
clipTo(padded);
const RectF rect = { padded.pos + mOrigin, padded.size };
NVGcontext* vg = mContext->vg;
nvgFontSize(vg, style.size);
nvgFontFaceId(vg, mContext->fonts.idFor(style.face));
nvgFillColor(vg, toNVGColor(style.color));
halnf anchorX = rect.x;
halnf anchorY = rect.y;
int alignment = 0;
switch (style.align.horizontal) {
case HorizontalAlign::Left: alignment |= NVG_ALIGN_LEFT; break;
case HorizontalAlign::Center:
alignment |= NVG_ALIGN_CENTER;
anchorX += rect.z * 0.5f;
break;
case HorizontalAlign::Right:
alignment |= NVG_ALIGN_RIGHT;
anchorX += rect.z;
break;
}
switch (style.align.vertical) {
case VerticalAlign::Top: alignment |= NVG_ALIGN_TOP; break;
case VerticalAlign::Center:
alignment |= NVG_ALIGN_MIDDLE;
anchorY += rect.w * 0.5f;
break;
case VerticalAlign::Bottom:
alignment |= NVG_ALIGN_BOTTOM;
anchorY += rect.w;
break;
}
nvgTextAlign(vg, alignment);
if (!isItalic(style.face)) {
nvgText(vg, anchorX, anchorY, string, nullptr);
return;
}
nvgTranslate(vg, anchorX, anchorY);
nvgSkewX(vg, kItalicSkew);
nvgText(vg, 0, 0, string, nullptr);
}
void Canvas::rotatedText(const char* string, Vec2F center, halnf size, halnf angle, const RGBA& color) {
center += mOrigin;
nvgSave(mContext->vg);
nvgTranslate(mContext->vg, center.x, center.y);
nvgRotate(mContext->vg, angle);
nvgFontSize(mContext->vg, size);
nvgFontFaceId(mContext->vg, mContext->fonts.regular);
nvgTextAlign(mContext->vg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
nvgFillColor(mContext->vg, toNVGColor(color));
nvgText(mContext->vg, 0, 0, string, nullptr);
nvgRestore(mContext->vg);
}
halnf Canvas::textWidth(const char* string, halnf size, FontFace face) {
nvgFontSize(mContext->vg, size);
nvgFontFaceId(mContext->vg, mContext->fonts.idFor(face));
return nvgTextBounds(mContext->vg, 0, 0, string, nullptr, nullptr);
}
void Canvas::prefixWidths(const char* string, size_t length, halnf size, FontFace face, halnf* widths) {
nvgFontSize(mContext->vg, size);
nvgFontFaceId(mContext->vg, mContext->fonts.idFor(face));
nvgTextAlign(mContext->vg, NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
std::vector<NVGglyphPosition> glyphs(length);
const int count = nvgTextGlyphPositions(mContext->vg, 0, 0, string, string + length, glyphs.data(), (int) length);
size_t byte = 0;
for (int index = 0; index < count; index++) {
const size_t glyphStart = (size_t) (glyphs[(size_t) index].str - string);
const halnf pen = glyphs[(size_t) index].x;
for (; byte <= glyphStart && byte < length; byte++) {
widths[byte] = pen;
}
}
const halnf total = nvgTextBounds(mContext->vg, 0, 0, string, string + length, nullptr);
for (; byte < length; byte++) {
widths[byte] = total;
}
widths[length] = total;
}
void Canvas::drawImage(RectF rect, ImageHandle image, halnf angle, halnf alpha, halnf rounding) {
if (!image.isValid()) {
return;
}
rect.pos += mOrigin;
const NVGpaint paint = nvgImagePattern(mContext->vg, rect.x, rect.y, rect.z, rect.w, angle, image.mId, alpha);
nvgBeginPath(mContext->vg);
nvgRoundedRect(mContext->vg, rect.x, rect.y, rect.z, rect.w, rounding);
nvgFillPaint(mContext->vg, paint);
nvgFill(mContext->vg);
}

View file

@ -0,0 +1,24 @@
#pragma once
#include "Canvas.hpp"
#include "CanvasFontLoader.hpp"
#include <nanovg.h>
namespace tp {
class Canvas::Context {
public:
NVGcontext* vg{};
WindowSurface* surface{};
Profiler* profiler{};
uint32_t flushSection = 0;
CanvasFontIds fonts;
// NanoVG reads the recording command buffer and frame slot through these pointers on every flush.
VkCommandBuffer frameCommandBuffers[WindowSurface::kFramesInFlight] = {};
uint32_t frameSlot = 0;
};
inline NVGcolor toNVGColor(const RGBA& color) { return { color.r, color.g, color.b, color.a }; }
}

View file

@ -0,0 +1,89 @@
#include "CanvasFontLoader.hpp"
#include "CanvasFonts.hpp"
#include <nanovg.h>
#include <cstring>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
using namespace tp;
namespace {
bool isBold(FontFace face) { return face == FontFace::Bold || face == FontFace::BoldItalic; }
class LoadedFallbackFonts {
public:
LoadedFallbackFonts(NVGcontext* vg, const std::filesystem::path& resourceDirectory) {
for (const FontFile& font : kFallbackFonts) {
const std::string path = (resourceDirectory / font.path).string();
mLoaded.emplace_back(font.name, nvgCreateFont(vg, font.name, path.c_str()));
}
}
template <size_t Size>
[[nodiscard]] std::vector<int> idsInChainOrder(const std::array<FontFile, Size>& chain) const {
std::vector<int> ids;
for (const FontFile& font : chain) {
const int id = idOf(font.name);
if (id != -1) {
ids.push_back(id);
}
}
return ids;
}
private:
[[nodiscard]] int idOf(const char* name) const {
for (const auto& [loaded, id] : mLoaded) {
if (std::strcmp(loaded, name) == 0) {
return id;
}
}
return -1;
}
std::vector<std::pair<const char*, int>> mLoaded;
};
int loadTextFont(
NVGcontext* vg,
const FontFile& font,
const std::vector<int>& fallbacks,
int missing,
const std::filesystem::path& resourceDirectory
) {
const std::string path = (resourceDirectory / font.path).string();
const int id = nvgCreateFont(vg, font.name, path.c_str());
if (id == -1) {
return missing;
}
for (const int fallback : fallbacks) {
nvgAddFallbackFontId(vg, id, fallback);
}
return id;
}
}
int CanvasFontIds::idFor(FontFace face) const {
if (face == FontFace::Monospace) {
return monospace;
}
return isBold(face) ? bold : regular;
}
CanvasFontIds tp::loadCanvasFonts(NVGcontext* vg, const std::filesystem::path& resourceDirectory) {
const LoadedFallbackFonts fallbacks(vg, resourceDirectory);
CanvasFontIds fonts;
fonts.regular = loadTextFont(vg, kRegularFont, fallbacks.idsInChainOrder(kRegularFallbacks), -1, resourceDirectory);
if (fonts.regular == -1) {
throw std::runtime_error("cannot load " + (resourceDirectory / kRegularFont.path).string());
}
fonts.bold = loadTextFont(vg, kBoldFont, fallbacks.idsInChainOrder(kBoldFallbacks), fonts.regular, resourceDirectory);
fonts.monospace =
loadTextFont(vg, kMonospaceFont, fallbacks.idsInChainOrder(kMonospaceFallbacks), fonts.regular, resourceDirectory);
return fonts;
}

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(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,70 @@
#include "CanvasContext.hpp"
#include "RenderTargetRegistry.hpp"
#include <nanovg_vk.h>
using namespace tp;
ImageHandle Canvas::createImageFromRenderTarget(uint32_t renderTargetId) {
RenderTargetEntry entry;
if (!mContext->surface->gpu().renderTargets().resolve(renderTargetId, entry)) {
return {};
}
const int image = nvgCreateImageFromHandleVk(
mContext->vg,
entry.image,
entry.view,
entry.sampler,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
entry.width,
entry.height,
0
);
return ImageHandle(image);
}
void Canvas::updateImageFromRenderTarget(ImageHandle image, uint32_t renderTargetId) {
RenderTargetEntry entry;
if (!image.isValid() || !mContext->surface->gpu().renderTargets().resolve(renderTargetId, entry)) {
return;
}
nvgUpdateImageFromHandleVk(
mContext->vg,
image.mId,
entry.image,
entry.view,
entry.sampler,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
entry.width,
entry.height
);
}
ImageHandle Canvas::createImageFromFile(const char* path) { return ImageHandle(nvgCreateImage(mContext->vg, path, 0)); }
ImageHandle Canvas::createImageFromMemoryRGBA(int width, int height, const unsigned char* rgba) {
return ImageHandle(nvgCreateImageRGBA(mContext->vg, width, height, 0, rgba));
}
void Canvas::updateImageRGBA(ImageHandle image, const unsigned char* rgba) {
if (image.isValid()) {
nvgUpdateImage(mContext->vg, image.mId, rgba);
}
}
Vec2F Canvas::imageSize(ImageHandle image) {
if (!image.isValid()) {
return { 0, 0 };
}
int width = 0;
int height = 0;
nvgImageSize(mContext->vg, image.mId, &width, &height);
return { (halnf) width, (halnf) height };
}
void Canvas::deleteImage(ImageHandle image) {
if (image.isValid()) {
nvgDeleteImage(mContext->vg, image.mId);
}
}

View file

@ -0,0 +1,183 @@
#pragma once
#include "Color.hpp"
#include "Rect.hpp"
#include "WindowFrame.hpp"
#include "WindowSurface.hpp"
#include "Buffer.hpp"
#include <filesystem>
#include <memory>
namespace tp {
class Profiler;
enum class HorizontalAlign { Left, Center, Right };
enum class VerticalAlign { Top, Center, Bottom };
struct TextAlign {
HorizontalAlign horizontal = HorizontalAlign::Left;
VerticalAlign vertical = VerticalAlign::Center;
static constexpr TextAlign leftCenter() { return { HorizontalAlign::Left, VerticalAlign::Center }; }
static constexpr TextAlign centered() { return { HorizontalAlign::Center, VerticalAlign::Center }; }
};
enum class FontFace { Regular, Bold, Italic, BoldItalic, Monospace };
struct TextStyle {
halnf size = 16;
RGBA color = RGBA(1, 1, 1, 1);
TextAlign align;
halnf padding = 0;
FontFace face = FontFace::Regular;
};
class ImageHandle {
public:
ImageHandle() = default;
[[nodiscard]] bool isValid() const { return mId > 0; }
[[nodiscard]] bool operator==(const ImageHandle& other) const = default;
private:
friend class Canvas;
explicit ImageHandle(int id) :
mId(id) {}
int mId = 0;
};
// 2D vector drawing for one window surface. Coordinates are relative to the current placement origin, which
// starts at the content origin; placements and clips are scoped.
class Canvas {
class Context;
public:
Canvas(WindowSurface& surface, Profiler& profiler, const std::filesystem::path& resourceDirectory);
~Canvas();
Canvas(const Canvas&) = delete;
Canvas& operator=(const Canvas&) = delete;
void drawBegin(const GpuFrame& frame, const WindowFrame& windowFrame);
void drawEnd();
enum class CompositeOperation { SourceOver, DestinationOut, DestinationOver };
enum class PathWinding { Solid, Hole };
struct LinearGradient {
Vec2F from;
Vec2F to;
RGBA fromColor;
RGBA toColor;
};
struct BoxGradient {
RectF box;
halnf radius = 0;
halnf feather = 0;
RGBA innerColor;
RGBA outerColor;
};
// Restores origin, clipping and composite operation when the scope ends.
class SavedState {
public:
explicit SavedState(Canvas& canvas) :
mCanvas(canvas) {
mCanvas.save();
}
~SavedState() { mCanvas.restore(); }
SavedState(const SavedState&) = delete;
SavedState& operator=(const SavedState&) = delete;
protected:
Canvas& mCanvas;
};
// Moves the origin by an offset from the current origin until destroyed.
class Translation : public SavedState {
public:
Translation(Canvas& canvas, Vec2F offset) :
SavedState(canvas) {
mCanvas.translateOrigin(offset);
}
};
// Places the origin at a point relative to the current origin and clips to an area there, until destroyed.
class Placement : public SavedState {
public:
Placement(Canvas& canvas, Vec2F origin, Vec2F clipSize) :
SavedState(canvas) {
mCanvas.translateOrigin(origin);
mCanvas.clipTo({ { 0, 0 }, clipSize });
}
};
[[nodiscard]] Vec2F origin() const { return mOrigin; }
[[nodiscard]] bool isClippedAway() const;
[[nodiscard]] bool isOutsideClip(const RectF& rect) const;
void setCompositeOperation(CompositeOperation operation);
void beginPath();
void addPathRect(RectF rect, halnf round = 0);
void setPathWinding(PathWinding winding);
void fillPath(const RGBA& color);
void fillPath(const BoxGradient& gradient);
void rect(RectF rect, const RGBA& color, halnf round = 0);
void gradientRect(RectF rect, const RGBA& from, const RGBA& to, bool horizontal, halnf round = 0);
void frame(RectF rect, const RGBA& color);
void debugCross(RectF rect, const RGBA& color);
void outline(RectF rect, const RGBA& color, halnf round, halnf width);
void circle(Vec2F center, halnf radius, const RGBA& color);
void circleOutline(Vec2F center, halnf radius, const RGBA& color, halnf width);
void line(Vec2F start, Vec2F end, const RGBA& color, halnf thickness);
void polyline(const Vec2F* points, int count, const RGBA& color, halnf width);
void polygon(const Vec2F* points, int count, const RGBA& color);
void polygon(const Vec2F* points, int count, const LinearGradient& gradient);
void ringSegment(
Vec2F center, halnf innerRadius, halnf outerRadius, halnf fromAngle, halnf toAngle, const LinearGradient& gradient
);
void radialGlow(Vec2F center, halnf innerRadius, halnf outerRadius, const RGBA& innerColor, const RGBA& outerColor);
void text(const char* text, const RectF& area, const TextStyle& style);
void rotatedText(const char* text, Vec2F center, halnf size, halnf angle, const RGBA& color);
[[nodiscard]] halnf textWidth(const char* text, halnf size, FontFace face = FontFace::Regular);
void prefixWidths(const char* text, size_t length, halnf size, FontFace face, halnf* widths);
[[nodiscard]] ImageHandle createImageFromRenderTarget(uint32_t renderTargetId);
void updateImageFromRenderTarget(ImageHandle image, uint32_t renderTargetId);
[[nodiscard]] ImageHandle createImageFromFile(const char* path);
[[nodiscard]] ImageHandle createImageFromMemoryRGBA(int width, int height, const unsigned char* rgba);
void updateImageRGBA(ImageHandle image, const unsigned char* rgba);
[[nodiscard]] Vec2F imageSize(ImageHandle image);
void deleteImage(ImageHandle image);
void drawImage(RectF rect, ImageHandle image, halnf angle = 0, halnf alpha = 1, halnf rounding = 0);
private:
struct SavedFrame {
Vec2F origin;
size_t clipDepth = 0;
};
void save();
void restore();
void translateOrigin(Vec2F offset) { mOrigin += offset; }
void clipTo(const RectF& rect);
void applyClip();
std::unique_ptr<Context> mContext;
Buffer<RectF> mClips;
Buffer<SavedFrame> mSavedFrames;
Vec2F mOrigin = { 0, 0 };
};
}

View file

@ -0,0 +1,126 @@
#include "DebugGUI.hpp"
#include "GpuError.hpp"
#include "ImGuiPlatform.hpp"
#include <imgui.h>
#include <imgui_impl_vulkan.h>
#include "implot.h"
#include <array>
namespace tp {
namespace {
constexpr float kFontSize = 18.f;
constexpr float kHeadlessFrameSeconds = 1.0f / 60.0f;
VkDescriptorPool createDescriptorPool(VkDevice device) {
const std::array<VkDescriptorPoolSize, 1> poolSizes = {
VkDescriptorPoolSize{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 64 },
};
const VkDescriptorPoolCreateInfo poolInfo{
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT,
.maxSets = 64,
.poolSizeCount = (uint32_t) poolSizes.size(),
.pPoolSizes = poolSizes.data(),
};
VkDescriptorPool pool = VK_NULL_HANDLE;
requireSuccess(vkCreateDescriptorPool(device, &poolInfo, nullptr, &pool), "vkCreateDescriptorPool");
return pool;
}
}
class DebugGUI::Context {
public:
Context(Window& window, DebugInterface mode) :
window(window),
mode(mode) {}
Window& window;
DebugInterface mode;
ImGuiContext* imgui = nullptr;
ImPlotContext* implot = nullptr;
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
std::unique_ptr<ImGuiPlatform> platform;
[[nodiscard]] bool isVisible() const { return mode == DebugInterface::Visible; }
};
}
using namespace tp;
DebugGUI::DebugGUI(Window& window, const std::filesystem::path& resourceDirectory, DebugInterface mode) :
mContext(std::make_unique<Context>(window, mode)) {
IMGUI_CHECKVERSION();
mContext->imgui = ImGui::CreateContext();
mContext->implot = ImPlot::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigInputTrickleEventQueue = false;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.IniFilename = nullptr;
const std::string fontPath = (resourceDirectory / "Font.ttf").string();
io.Fonts->AddFontFromFileTTF(fontPath.c_str(), kFontSize);
if (!mContext->isVisible()) {
io.Fonts->Build();
return;
}
WindowSurface& surface = window.surface();
const GpuDevice& gpu = surface.gpu();
mContext->platform = std::make_unique<ImGuiPlatform>(window);
mContext->descriptorPool = createDescriptorPool(gpu.device());
ImGui_ImplVulkan_InitInfo initInfo{};
initInfo.Instance = gpu.instance();
initInfo.PhysicalDevice = gpu.physicalDevice();
initInfo.Device = gpu.device();
initInfo.QueueFamily = gpu.graphicsQueueFamily();
initInfo.Queue = gpu.graphicsQueue();
initInfo.DescriptorPool = mContext->descriptorPool;
initInfo.RenderPass = surface.renderPass();
initInfo.MinImageCount = WindowSurface::kFramesInFlight;
initInfo.ImageCount = std::max(surface.imageCount(), WindowSurface::kFramesInFlight);
initInfo.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
ImGui_ImplVulkan_Init(&initInfo);
}
DebugGUI::~DebugGUI() {
if (mContext->isVisible()) {
mContext->window.surface().gpu().waitIdle();
ImGui_ImplVulkan_Shutdown();
mContext->platform.reset();
vkDestroyDescriptorPool(mContext->window.surface().gpu().device(), mContext->descriptorPool, nullptr);
}
ImPlot::DestroyContext(mContext->implot);
ImGui::DestroyContext(mContext->imgui);
}
DebugInterface DebugGUI::mode() const { return mContext->mode; }
void DebugGUI::drawBegin() {
if (mContext->isVisible()) {
ImGui_ImplVulkan_NewFrame();
mContext->platform->newFrame();
} else {
const Vec2F size = mContext->window.contentSize();
ImGuiIO& io = ImGui::GetIO();
io.DisplaySize = ImVec2(std::max(size.x, 1.0f), std::max(size.y, 1.0f));
io.DeltaTime = kHeadlessFrameSeconds;
}
ImGui::NewFrame();
}
void DebugGUI::drawEnd(const GpuFrame& frame) {
ImGui::Render();
if (!mContext->isVisible()) {
return;
}
mContext->platform->endFrame();
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), frame.commandBuffer);
}
void DebugGUI::showValue(const char* label, halnf value) { ImGui::Text("%s: %f", label, value); }

View file

@ -0,0 +1,280 @@
#include "ImGuiPlatform.hpp"
#include "Timing.hpp"
#include "Window.hpp"
#include <imgui.h>
#include <cfloat>
#include <unordered_map>
using namespace tp;
namespace {
constexpr double kFirstFrameSeconds = 1.0 / 60.0;
ImGuiKey imguiKey(Key key) {
static const std::unordered_map<Key, ImGuiKey> keys = {
{ Key::Tab, ImGuiKey_Tab },
{ Key::Left, ImGuiKey_LeftArrow },
{ Key::Right, ImGuiKey_RightArrow },
{ Key::Up, ImGuiKey_UpArrow },
{ Key::Down, ImGuiKey_DownArrow },
{ Key::PageUp, ImGuiKey_PageUp },
{ Key::PageDown, ImGuiKey_PageDown },
{ Key::Home, ImGuiKey_Home },
{ Key::End, ImGuiKey_End },
{ Key::Insert, ImGuiKey_Insert },
{ Key::Delete, ImGuiKey_Delete },
{ Key::Backspace, ImGuiKey_Backspace },
{ Key::Space, ImGuiKey_Space },
{ Key::Enter, ImGuiKey_Enter },
{ Key::Escape, ImGuiKey_Escape },
{ Key::LeftControl, ImGuiKey_LeftCtrl },
{ Key::LeftShift, ImGuiKey_LeftShift },
{ Key::LeftAlt, ImGuiKey_LeftAlt },
{ Key::LeftSuper, ImGuiKey_LeftSuper },
{ Key::RightControl, ImGuiKey_RightCtrl },
{ Key::RightShift, ImGuiKey_RightShift },
{ Key::RightAlt, ImGuiKey_RightAlt },
{ Key::RightSuper, ImGuiKey_RightSuper },
{ Key::Menu, ImGuiKey_Menu },
{ Key::Apostrophe, ImGuiKey_Apostrophe },
{ Key::Comma, ImGuiKey_Comma },
{ Key::Minus, ImGuiKey_Minus },
{ Key::Period, ImGuiKey_Period },
{ Key::Slash, ImGuiKey_Slash },
{ Key::Semicolon, ImGuiKey_Semicolon },
{ Key::Equal, ImGuiKey_Equal },
{ Key::LeftBracket, ImGuiKey_LeftBracket },
{ Key::Backslash, ImGuiKey_Backslash },
{ Key::RightBracket, ImGuiKey_RightBracket },
{ Key::GraveAccent, ImGuiKey_GraveAccent },
{ Key::CapsLock, ImGuiKey_CapsLock },
{ Key::ScrollLock, ImGuiKey_ScrollLock },
{ Key::NumLock, ImGuiKey_NumLock },
{ Key::PrintScreen, ImGuiKey_PrintScreen },
{ Key::Pause, ImGuiKey_Pause },
{ Key::KeypadDecimal, ImGuiKey_KeypadDecimal },
{ Key::KeypadDivide, ImGuiKey_KeypadDivide },
{ Key::KeypadMultiply, ImGuiKey_KeypadMultiply },
{ Key::KeypadSubtract, ImGuiKey_KeypadSubtract },
{ Key::KeypadAdd, ImGuiKey_KeypadAdd },
{ Key::KeypadEnter, ImGuiKey_KeypadEnter },
{ Key::KeypadEqual, ImGuiKey_KeypadEqual },
};
const auto found = keys.find(key);
if (found != keys.end()) {
return found->second;
}
const auto code = (int) key;
if (code >= (int) Key::A && code <= (int) Key::Z) {
return (ImGuiKey) (ImGuiKey_A + code - (int) Key::A);
}
if (code >= (int) Key::Digit0 && code <= (int) Key::Digit9) {
return (ImGuiKey) (ImGuiKey_0 + code - (int) Key::Digit0);
}
if (code >= (int) Key::F1 && code <= (int) Key::F24) {
return (ImGuiKey) (ImGuiKey_F1 + code - (int) Key::F1);
}
if (code >= (int) Key::Keypad0 && code <= (int) Key::Keypad9) {
return (ImGuiKey) (ImGuiKey_Keypad0 + code - (int) Key::Keypad0);
}
return ImGuiKey_None;
}
ImGuiKey imguiModifier(Key key) {
switch (key) {
case Key::LeftControl:
case Key::RightControl: return ImGuiMod_Ctrl;
case Key::LeftShift:
case Key::RightShift: return ImGuiMod_Shift;
case Key::LeftAlt:
case Key::RightAlt: return ImGuiMod_Alt;
case Key::LeftSuper:
case Key::RightSuper: return ImGuiMod_Super;
default: return ImGuiKey_None;
}
}
bool isRightModifier(Key key) {
return key == Key::RightControl || key == Key::RightShift || key == Key::RightAlt || key == Key::RightSuper;
}
int modifierIndex(ImGuiKey modifier) {
switch (modifier) {
case ImGuiMod_Ctrl: return 0;
case ImGuiMod_Shift: return 1;
case ImGuiMod_Alt: return 2;
default: return 3;
}
}
int imguiMouseButton(PointerButton button) {
switch (button) {
case PointerButton::Left: return ImGuiMouseButton_Left;
case PointerButton::Right: return ImGuiMouseButton_Right;
case PointerButton::Middle: return ImGuiMouseButton_Middle;
case PointerButton::Back: return 3;
case PointerButton::Forward: return 4;
}
return -1;
}
CursorShape cursorShape(ImGuiMouseCursor cursor) {
switch (cursor) {
case ImGuiMouseCursor_TextInput: return CursorShape::Text;
case ImGuiMouseCursor_ResizeAll: return CursorShape::Move;
case ImGuiMouseCursor_ResizeNS: return CursorShape::ResizeVertical;
case ImGuiMouseCursor_ResizeEW: return CursorShape::ResizeHorizontal;
case ImGuiMouseCursor_ResizeNESW: return CursorShape::ResizeNorthEastSouthWest;
case ImGuiMouseCursor_ResizeNWSE: return CursorShape::ResizeNorthWestSouthEast;
case ImGuiMouseCursor_Hand: return CursorShape::Pointer;
case ImGuiMouseCursor_NotAllowed: return CursorShape::NotAllowed;
default: return CursorShape::Default;
}
}
}
ImGuiPlatform::ImGuiPlatform(Window& window) :
mWindow(window) {
ImGuiIO& io = ImGui::GetIO();
io.BackendPlatformName = "graphic_application";
io.BackendPlatformUserData = this;
io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors;
io.SetClipboardTextFn = &ImGuiPlatform::writeClipboard;
io.GetClipboardTextFn = &ImGuiPlatform::readClipboard;
io.ClipboardUserData = this;
io.SetPlatformImeDataFn = &ImGuiPlatform::placeInputMethod;
}
ImGuiPlatform::~ImGuiPlatform() {
ImGuiIO& io = ImGui::GetIO();
io.BackendPlatformName = nullptr;
io.BackendPlatformUserData = nullptr;
io.SetClipboardTextFn = nullptr;
io.GetClipboardTextFn = nullptr;
io.SetPlatformImeDataFn = nullptr;
}
void ImGuiPlatform::newFrame() {
consumeInput(mWindow.input().state());
ImGuiIO& io = ImGui::GetIO();
const Vec2F size = mWindow.contentSize();
const Vec2F framebuffer = mWindow.framebufferSize();
const Vec2F surface = mWindow.frame().surfaceSize();
const halnf scale = surface.x > 0 ? framebuffer.x / surface.x : 1;
io.DisplaySize = ImVec2(size.x, size.y);
io.DisplayFramebufferScale = ImVec2(scale, scale);
const double now = (double) get_time_ns() / 1.0e9;
io.DeltaTime = mPreviousTime > 0 && now > mPreviousTime ? (float) (now - mPreviousTime) : (float) kFirstFrameSeconds;
mPreviousTime = now;
updateCursor();
}
void ImGuiPlatform::endFrame() {
ImDrawData* drawData = ImGui::GetDrawData();
if (!drawData) {
return;
}
const halnf margin = mWindow.frame().margin();
drawData->DisplayPos = ImVec2(-margin, -margin);
drawData->DisplaySize = ImVec2(mWindow.frame().surfaceSize().x, mWindow.frame().surfaceSize().y);
const bool wantsText = ImGui::GetIO().WantTextInput;
if (wantsText != mTextInputWanted) {
mTextInputWanted = wantsText;
mWindow.services().setTextInputEnabled(wantsText);
}
}
void ImGuiPlatform::consumeInput(const InputState& input) {
ImGuiIO& io = ImGui::GetIO();
for (const InputEvent& event : input.events()) {
std::visit(
InputEventVisitor{
[&io](const PointerMoved& moved) { io.AddMousePosEvent(moved.position.x, moved.position.y); },
[&io](const PointerLeft&) { io.AddMousePosEvent(-FLT_MAX, -FLT_MAX); },
[&io](const PointerButtonChanged& changed) {
io.AddMouseButtonEvent(imguiMouseButton(changed.button), changed.pressed);
},
[&io](const Scrolled& scrolled) { io.AddMouseWheelEvent(scrolled.delta.x, scrolled.delta.y); },
[this](const KeyChanged& changed) { onKey(changed.key, changed.action != KeyAction::Release); },
[&io](const TextEntered& text) { io.AddInputCharactersUTF8(text.utf8.c_str()); },
[&io](const KeyboardFocusChanged& focus) { io.AddFocusEvent(focus.focused); },
[](const PointerSampled&) {},
[](const ScrollEnded&) {},
[](const GestureChanged&) {},
},
event
);
}
}
void ImGuiPlatform::onKey(Key key, bool pressed) {
updateModifier(key, pressed);
const ImGuiKey imgui = imguiKey(key);
if (imgui != ImGuiKey_None) {
ImGui::GetIO().AddKeyEvent(imgui, pressed);
}
}
void ImGuiPlatform::updateModifier(Key key, bool pressed) {
const ImGuiKey modifier = imguiModifier(key);
if (modifier == ImGuiKey_None) {
return;
}
const size_t side = isRightModifier(key) ? 1 : 0;
mModifierSides[(size_t) modifierIndex(modifier)][side] = pressed;
const bool down =
mModifierSides[(size_t) modifierIndex(modifier)][0] || mModifierSides[(size_t) modifierIndex(modifier)][1];
ImGui::GetIO().AddKeyEvent(modifier, down);
}
void ImGuiPlatform::updateCursor() {
ImGuiIO& io = ImGui::GetIO();
if (io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange) {
return;
}
if (!io.WantCaptureMouse) {
mCursor = -2;
return;
}
const ImGuiMouseCursor cursor = ImGui::GetMouseCursor();
if (cursor == mCursor) {
return;
}
mCursor = cursor;
mWindow.services().setCursor(cursorShape(cursor));
}
const char* ImGuiPlatform::readClipboard(void* userData) {
auto* self = static_cast<ImGuiPlatform*>(userData);
self->mClipboard = self->mWindow.services().clipboardText();
return self->mClipboard.c_str();
}
void ImGuiPlatform::writeClipboard(void* userData, const char* text) {
static_cast<ImGuiPlatform*>(userData)->mWindow.services().setClipboardText(text ? text : "");
}
void ImGuiPlatform::placeInputMethod(ImGuiViewport*, ImGuiPlatformImeData* data) {
auto* self = static_cast<ImGuiPlatform*>(ImGui::GetIO().BackendPlatformUserData);
if (!self || !data->WantVisible) {
return;
}
self->mWindow.services().setTextInputRect({ data->InputPos.x, data->InputPos.y, 1, data->InputLineHeight });
}

View file

@ -0,0 +1,43 @@
#pragma once
#include "InputState.hpp"
#include <string>
struct ImGuiViewport;
struct ImGuiPlatformImeData;
namespace tp {
class Window;
class ImGuiPlatform {
public:
explicit ImGuiPlatform(Window& window);
~ImGuiPlatform();
ImGuiPlatform(const ImGuiPlatform&) = delete;
ImGuiPlatform& operator=(const ImGuiPlatform&) = delete;
void newFrame();
void endFrame();
private:
void consumeInput(const InputState& input);
void onKey(Key key, bool pressed);
static const char* readClipboard(void* userData);
static void writeClipboard(void* userData, const char* text);
static void placeInputMethod(ImGuiViewport* viewport, ImGuiPlatformImeData* data);
void updateCursor();
void updateModifier(Key key, bool pressed);
Window& mWindow;
std::string mClipboard;
double mPreviousTime = 0;
int mCursor = -2;
bool mModifierSides[4][2] = {};
bool mTextInputWanted = false;
};
}

View file

@ -0,0 +1,35 @@
#pragma once
#include "Window.hpp"
#include <filesystem>
#include <memory>
namespace tp {
// Visible renders ImGui into the window and feeds it input; Headless keeps the ImGui context and frames alive
// for code that calls ImGui directly, without drawing or taking input.
enum class DebugInterface { Visible, Headless };
// Owns the process ImGui and ImPlot contexts. ImGui calls are valid between drawBegin and drawEnd.
class DebugGUI {
class Context;
public:
DebugGUI(Window& window, const std::filesystem::path& resourceDirectory, DebugInterface mode);
~DebugGUI();
DebugGUI(const DebugGUI&) = delete;
DebugGUI& operator=(const DebugGUI&) = delete;
[[nodiscard]] DebugInterface mode() const;
void drawBegin();
void drawEnd(const GpuFrame& frame);
void showValue(const char* label, halnf value);
private:
std::unique_ptr<Context> mContext;
};
}

View file

@ -0,0 +1,55 @@
#include "WindowDecorations.hpp"
using namespace tp;
namespace {
const RGBA kTransparent = { 0, 0, 0, 0 };
const RGBA kOpaque = { 0, 0, 0, 1 };
RectF contentArea(const WindowFrame& frame) { return { { 0, 0 }, frame.contentSize() }; }
}
void WindowDecorations::paintBase(Canvas& canvas, const WindowFrame& frame) const {
canvas.rect(contentArea(frame), mStyle.base);
}
void WindowDecorations::paintFrame(Canvas& canvas, const WindowFrame& frame, bool focused) const {
if (!frame.hasShadow()) {
return;
}
const Canvas::SavedState saved(canvas);
cutCorners(canvas, frame);
paintOutline(canvas, frame);
paintShadow(canvas, frame, focused);
}
void WindowDecorations::cutCorners(Canvas& canvas, const WindowFrame& frame) const {
canvas.setCompositeOperation(Canvas::CompositeOperation::DestinationOut);
canvas.beginPath();
canvas.addPathRect(contentArea(frame));
canvas.addPathRect(contentArea(frame), frame.cornerRadius());
canvas.setPathWinding(Canvas::PathWinding::Hole);
canvas.fillPath(kOpaque);
}
void WindowDecorations::paintOutline(Canvas& canvas, const WindowFrame& frame) const {
canvas.setCompositeOperation(Canvas::CompositeOperation::SourceOver);
canvas.outline(contentArea(frame), mStyle.outline, frame.cornerRadius(), mStyle.outlineWidth);
}
void WindowDecorations::paintShadow(Canvas& canvas, const WindowFrame& frame, bool focused) const {
const halnf margin = frame.margin();
const Canvas::BoxGradient shadow = {
.box = RectF(Vec2F(0.0f, mStyle.shadowOffsetY), frame.contentSize()),
.radius = frame.cornerRadius(),
.feather = margin * mStyle.shadowFeatherFraction,
.innerColor = focused ? mStyle.focusedShadow : mStyle.unfocusedShadow,
.outerColor = kTransparent,
};
canvas.setCompositeOperation(Canvas::CompositeOperation::DestinationOver);
canvas.beginPath();
canvas.addPathRect({ { -margin, -margin }, frame.surfaceSize() });
canvas.fillPath(shadow);
}

View file

@ -0,0 +1,36 @@
#pragma once
#include "Canvas.hpp"
#include "WindowFrame.hpp"
namespace tp {
struct WindowDecorationStyle {
RGBA base = { 0.118f, 0.122f, 0.133f, 1.0f };
RGBA outline = { 1.0f, 1.0f, 1.0f, 0.07f };
RGBA focusedShadow = { 0, 0, 0, 0.42f };
RGBA unfocusedShadow = { 0, 0, 0, 0.24f };
halnf shadowFeatherFraction = 0.9f;
halnf shadowOffsetY = 3;
halnf outlineWidth = 1;
};
// Client-side decorations of a translucent desktop window: base, rounded corners, outline and drop shadow.
// Geometry and edge hit-testing come from WindowFrame, which the backend uses for input; this paints the same frame.
class WindowDecorations {
public:
WindowDecorations() = default;
explicit WindowDecorations(const WindowDecorationStyle& style) :
mStyle(style) {}
void paintBase(Canvas& canvas, const WindowFrame& frame) const;
void paintFrame(Canvas& canvas, const WindowFrame& frame, bool focused) const;
private:
void cutCorners(Canvas& canvas, const WindowFrame& frame) const;
void paintOutline(Canvas& canvas, const WindowFrame& frame) const;
void paintShadow(Canvas& canvas, const WindowFrame& frame, bool focused) const;
WindowDecorationStyle mStyle;
};
}

View file

@ -0,0 +1,393 @@
#include "HangWatchdog.hpp"
#include "StackTrace.hpp"
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <ctime>
#include <fstream>
#include <sstream>
#if defined(__linux__)
#include <sys/prctl.h>
#endif
namespace tp {
namespace {
int64_t nowNs() {
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch())
.count();
}
std::string timestamp(const char* pattern) {
const std::time_t now = std::time(nullptr);
char text[64];
std::strftime(text, sizeof(text), pattern, std::localtime(&now));
return text;
}
void writeFile(const std::filesystem::path& path, const std::string& text, std::ios::openmode mode) {
std::ofstream file(path, mode);
file << text;
}
constexpr double kCheckIntervalMs = 100;
}
HangWatchdog::HangWatchdog(HangSettings settings, UiTrail& trail) :
mSettings(std::move(settings)),
mTrail(trail) {
mSettings.directory = directoryFor(mSettings.directory.empty() ? defaultHangsDirectory() : mSettings.directory);
if (!mSettings.clockNs) {
mSettings.clockNs = nowNs;
}
mPhaseStartNs.store(now());
allowDebuggerAttach();
StackTrace::prepareThreadSampling();
mUiTid = StackTrace::callingThreadId();
mTrail.trackCallingThread();
pruneOldReports();
mThread = std::thread([this]() { watch(); });
}
HangWatchdog::~HangWatchdog() {
{
std::lock_guard lock(mStopMutex);
mStopping = true;
}
mStopped.notify_all();
mThread.join();
mTrail.stopTracking();
}
std::string HangWatchdog::formatThreads(const std::vector<ThreadSample>& threads, int uiTid) {
std::ostringstream text;
for (const ThreadSample& thread : threads) {
text << (thread.tid == uiTid ? "UI THREAD " : "thread ") << thread.tid << " \"" << thread.name << "\" state "
<< thread.state;
if (!thread.waitChannel.empty() && thread.waitChannel != "0") {
text << " waiting in " << thread.waitChannel;
}
text << "\n";
if (!thread.answered) {
text << " (did not answer the sample signal)\n";
}
for (uintptr_t frame : thread.frames) {
text << " " << StackTrace::describe(frame) << "\n";
}
text << "\n";
}
return text.str();
}
void HangWatchdog::enterPhase(UiPhase phase) {
mPhaseStartNs.store(now(), std::memory_order_relaxed);
mPhaseInputEvents.store(mInputEvents.load(std::memory_order_relaxed), std::memory_order_relaxed);
mPhase.store(phase, std::memory_order_relaxed);
mBeat.fetch_add(1, std::memory_order_release);
}
void HangWatchdog::noteInputEvents(uint64_t count) { mInputEvents.fetch_add(count, std::memory_order_relaxed); }
int64_t HangWatchdog::now() const { return mSettings.clockNs(); }
void HangWatchdog::addReportFileSource(ReportFileSource reportFileSource) {
std::lock_guard lock(mReportFileSourcesMutex);
mReportFileSources.push_back(std::move(reportFileSource));
}
std::filesystem::path HangWatchdog::defaultHangsDirectory() {
const char* home = std::getenv("HOME");
const std::filesystem::path root = home && *home ? std::filesystem::path(home) : std::filesystem::current_path();
return root / processName() / "hangs";
}
std::filesystem::path HangWatchdog::directoryFor(const std::filesystem::path& applicationHangsDirectory) {
if (const char* configured = std::getenv("MODULES_HANG_DIR"); configured && *configured) {
return configured;
}
return applicationHangsDirectory;
}
std::string HangWatchdog::processName() {
#if defined(__linux__)
std::ifstream comm("/proc/self/comm");
std::string name;
if (std::getline(comm, name) && !name.empty()) {
return name;
}
#endif
return "app";
}
void HangWatchdog::allowDebuggerAttach() {
#if defined(__linux__) && defined(PR_SET_PTRACER)
prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0);
#endif
}
const char* HangWatchdog::phaseName(UiPhase phase) {
switch (phase) {
case UiPhase::Idle: return "idle";
case UiPhase::Input: return "processing input";
case UiPhase::Draw: return "drawing";
case UiPhase::Present: return "presenting";
}
return "unknown";
}
void HangWatchdog::watch() {
Stall stall;
int64_t previous = now();
std::unique_lock lock(mStopMutex);
while (!mStopping) {
mStopped.wait_for(lock, std::chrono::duration<double, std::milli>(kCheckIntervalMs), [this]() {
return mStopping;
});
if (mStopping) {
return;
}
lock.unlock();
const int64_t current = now();
discountProcessPause(current, previous);
inspect(stall, current);
previous = current;
mInspections.fetch_add(1);
lock.lock();
}
}
void HangWatchdog::discountProcessPause(int64_t now, int64_t previous) {
const double sinceLastCheckMs = (double) (now - previous) / 1.0e6;
const double pausedMs = sinceLastCheckMs - kCheckIntervalMs;
if (pausedMs < mSettings.processPauseMs) {
return;
}
const uint64_t beat = mBeat.load(std::memory_order_acquire);
if (beat != mPausedBeat) {
mPausedBeat = beat;
mPausedNs = 0;
}
mPausedNs += (int64_t) (pausedMs * 1.0e6);
}
double HangWatchdog::stalledMs(int64_t now) const {
const bool pausedThisPhase = mPausedBeat == mBeat.load(std::memory_order_acquire);
const int64_t paused = pausedThisPhase ? mPausedNs : 0;
return (double) (now - mPhaseStartNs.load(std::memory_order_relaxed) - paused) / 1.0e6;
}
uint64_t HangWatchdog::inputEventsInPhase() const {
return mInputEvents.load(std::memory_order_relaxed) - mPhaseInputEvents.load(std::memory_order_relaxed);
}
void HangWatchdog::inspect(Stall& stall, int64_t now) {
const uint64_t beat = mBeat.load(std::memory_order_acquire);
const UiPhase phase = mPhase.load(std::memory_order_relaxed);
const double stalled = stalledMs(now);
if (!stall.report.empty() && stall.beat != beat) {
finishRecovered(stall, stalled);
stall = {};
}
if (phase == UiPhase::Idle || stalled < mSettings.thresholdMs) {
return;
}
if (stall.report.empty()) {
stall.beat = beat;
stall.phase = phase;
beginReport(stall, stalled);
return;
}
if (stall.followUps < mSettings.followUpMs.size() && stalled >= mSettings.followUpMs[stall.followUps]) {
appendFollowUp(stall, stalled);
}
}
std::string HangWatchdog::uiSnapshot(double stalled, UiPhase phase) const {
std::ostringstream text;
text << "ui thread " << phaseName(phase) << " for " << (int64_t) stalled << " ms\n";
text << "input events handled during this phase " << inputEventsInPhase() << "\n";
text << "scope trail (outermost first):\n";
const std::vector<std::string> names = mTrail.snapshot();
if (names.empty()) {
text << " (no profiled scope open)\n";
}
for (size_t index = 0; index < names.size(); index++) {
text << " " << std::string(index * 2, ' ') << names[index] << "\n";
}
return text.str();
}
std::filesystem::path HangWatchdog::newReportPath() const {
const std::string base = mSettings.application + "-" + timestamp("%Y-%m-%d_%H-%M-%S") + "-build" + mSettings.build;
std::filesystem::path path = mSettings.directory / base;
for (int suffix = 2; std::filesystem::exists(path); suffix++) {
path = mSettings.directory / (base + "-" + std::to_string(suffix));
}
return path;
}
std::string HangWatchdog::reportHeader() const {
std::ostringstream text;
text << "application " << mSettings.application << "\n";
text << "build " << mSettings.build << "\n";
text << "detected " << timestamp("%Y-%m-%d %H:%M:%S") << "\n";
text << "threshold " << (int64_t) mSettings.thresholdMs << " ms\n";
text << "debugger attached " << (StackTrace::isDebuggerAttached() ? "yes" : "no") << "\n\n";
return text.str();
}
void HangWatchdog::beginReport(Stall& stall, double stalled) {
std::error_code error;
std::filesystem::create_directories(mSettings.directory, error);
stall.report = newReportPath();
std::filesystem::create_directories(stall.report, error);
if (error) {
return;
}
writeFile(
stall.report / "summary.txt", reportHeader() + uiSnapshot(stalled, stall.phase), std::ios::out | std::ios::trunc
);
writeSourcedFiles(stall.report);
mReportsWritten.fetch_add(1);
const Investigation investigation = investigate();
writeFile(
stall.report / "summary.txt", "\nverdict: " + investigation.verdict + "\n", std::ios::out | std::ios::app
);
if (!investigation.threads.empty()) {
writeFile(stall.report / "threads.txt", investigation.threads, std::ios::out | std::ios::trunc);
}
pruneOldReports();
}
void HangWatchdog::writeSourcedFiles(const std::filesystem::path& report) {
std::vector<ReportFileSource> reportFileSources;
{
std::lock_guard lock(mReportFileSourcesMutex);
reportFileSources = mReportFileSources;
}
for (const ReportFileSource& reportFileSource : reportFileSources) {
for (const HangReportFile& file : reportFileSource()) {
writeFile(report / file.fileName, file.text, std::ios::out | std::ios::trunc);
}
}
}
void HangWatchdog::appendFollowUp(Stall& stall, double stalled) {
stall.followUps++;
const std::string header = "\nstill stuck at " + timestamp("%H:%M:%S") + "\n";
writeFile(stall.report / "summary.txt", header + uiSnapshot(stalled, stall.phase), std::ios::out | std::ios::app);
const Investigation investigation = investigate();
writeFile(stall.report / "summary.txt", "verdict: " + investigation.verdict + "\n", std::ios::out | std::ios::app);
if (!investigation.threads.empty()) {
writeFile(
stall.report / "threads.txt",
"\n===== follow-up at " + timestamp("%H:%M:%S") + " =====\n" + investigation.threads,
std::ios::out | std::ios::app
);
}
}
void HangWatchdog::finishRecovered(Stall& stall, double stalled) {
(void) stalled;
writeFile(
stall.report / "summary.txt", "\nrecovered at " + timestamp("%H:%M:%S") + "\n", std::ios::out | std::ios::app
);
}
HangWatchdog::Investigation HangWatchdog::investigate() const {
if (StackTrace::isDebuggerAttached()) {
return { "not sampled: a debugger is attached, its thread view shows where the ui thread is", "" };
}
if (!StackTrace::canSampleThreads()) {
return { "not sampled: thread stacks are not available on this platform", "" };
}
std::vector<std::vector<uintptr_t>> uiSamples;
std::string uiState;
std::string uiWait;
std::string threads = sampleStacks(uiSamples, uiState, uiWait);
std::string verdict = stackVerdict(uiSamples, uiState, uiWait);
if (inputEventsInPhase() > 0) {
verdict +=
"; " + std::to_string(inputEventsInPhase()) + " input events were handled without leaving the input phase";
}
return { verdict, threads };
}
std::string HangWatchdog::sampleStacks(
std::vector<std::vector<uintptr_t>>& uiSamples, std::string& uiState, std::string& uiWait
) const {
std::ostringstream text;
for (int sample = 0; sample < std::max(1, mSettings.stackSamples); sample++) {
if (sample > 0) {
std::this_thread::sleep_for(std::chrono::duration<double, std::milli>(mSettings.stackSampleIntervalMs));
}
const std::vector<ThreadSample> threads = StackTrace::sampleAllThreads();
for (const ThreadSample& thread : threads) {
if (thread.tid == mUiTid) {
uiSamples.push_back(thread.frames);
uiState = thread.state;
uiWait = thread.waitChannel;
}
}
text << "===== sample " << sample + 1 << " =====\n" << formatThreads(threads, mUiTid);
}
return text.str();
}
std::string HangWatchdog::stackVerdict(
const std::vector<std::vector<uintptr_t>>& uiSamples, const std::string& state, const std::string& waitChannel
) {
if (uiSamples.empty()) {
return "ui thread not found";
}
const bool sameStack =
std::all_of(uiSamples.begin(), uiSamples.end(), [&](const auto& frames) { return frames == uiSamples.front(); });
const bool sleeping = !state.empty() && (state[0] == 'S' || state[0] == 'D');
if (sameStack && sleeping) {
std::string text = "blocked: the ui thread sleeps at the same place in every sample";
if (!waitChannel.empty() && waitChannel != "0") {
text += " (kernel wait " + waitChannel + ")";
}
return text;
}
if (!sleeping) {
return "busy: the ui thread is running, check the changing frames in threads.txt";
}
return "waiting repeatedly: the ui thread sleeps but at different places, likely polling or lock contention";
}
std::vector<std::filesystem::path> HangWatchdog::reports() const {
std::vector<std::filesystem::path> found;
std::error_code error;
for (const auto& entry : std::filesystem::directory_iterator(mSettings.directory, error)) {
if (entry.is_directory()) {
found.push_back(entry.path());
}
}
std::sort(found.begin(), found.end(), [](const auto& a, const auto& b) {
return std::filesystem::last_write_time(a) < std::filesystem::last_write_time(b);
});
return found;
}
void HangWatchdog::pruneOldReports() const {
const std::vector<std::filesystem::path> found = reports();
if (found.size() <= mSettings.keptReports) {
return;
}
std::error_code error;
for (size_t index = 0; index + mSettings.keptReports < found.size(); index++) {
std::filesystem::remove_all(found[index], error);
}
}
}

View file

@ -0,0 +1,219 @@
#include "Profiler.hpp"
#include <algorithm>
#include <cmath>
#include <ctime>
#include <sys/resource.h>
namespace tp {
namespace {
double clockMs(clockid_t clock) {
timespec now{};
clock_gettime(clock, &now);
return (double) now.tv_sec * 1000.0 + (double) now.tv_nsec / 1.0e6;
}
int64_t monotonicNs() {
timespec now{};
clock_gettime(CLOCK_MONOTONIC, &now);
return (int64_t) now.tv_sec * 1000000000ll + now.tv_nsec;
}
double percentile(const std::vector<double>& sorted, double fraction) {
if (sorted.empty()) {
return 0;
}
const double position = fraction * (double) (sorted.size() - 1);
const size_t lower = (size_t) std::floor(position);
const size_t upper = std::min(lower + 1, sorted.size() - 1);
const double blend = position - (double) lower;
return sorted[lower] + (sorted[upper] - sorted[lower]) * blend;
}
}
ProfileStatistics summarize(std::vector<double> values) {
ProfileStatistics statistics;
statistics.samples = values.size();
if (values.empty()) {
return statistics;
}
std::sort(values.begin(), values.end());
for (double value : values) {
statistics.total += value;
}
statistics.mean = statistics.total / (double) values.size();
statistics.minimum = values.front();
statistics.maximum = values.back();
statistics.median = percentile(values, 0.5);
statistics.p95 = percentile(values, 0.95);
statistics.p99 = percentile(values, 0.99);
return statistics;
}
void Profiler::setEnabled(bool enabled) {
mEnabled = enabled;
if (!enabled) {
mInFrame = false;
}
}
ProfileId Profiler::intern(std::deque<std::string>& names, const char* name) {
const ptrdiff_t existing = indexOf(names, name);
if (existing >= 0) {
return (ProfileId) existing;
}
if (names.size() >= kMaxNames) {
return (ProfileId) (kMaxNames - 1);
}
names.emplace_back(name);
return (ProfileId) (names.size() - 1);
}
ptrdiff_t Profiler::indexOf(const std::deque<std::string>& names, const std::string& name) {
const auto found = std::find(names.begin(), names.end(), name);
return found == names.end() ? -1 : found - names.begin();
}
ProfileId Profiler::section(const char* name) { return intern(mSectionNames, name); }
ProfileId Profiler::counter(const char* name) { return intern(mCounterNames, name); }
const char* Profiler::sectionName(ProfileId section) const {
return section < mSectionNames.size() ? mSectionNames[section].c_str() : "?";
}
void Profiler::beginFrame() {
if (!mEnabled) {
return;
}
mSectionMs.fill(0);
mSectionCalls.fill(0);
mCounters.fill(0);
mFrameWallStart = clockMs(CLOCK_MONOTONIC);
mFrameCpuStart = threadCpuMs();
mInFrame = true;
}
void Profiler::endFrame() {
if (!mEnabled || !mInFrame) {
return;
}
mInFrame = false;
FrameProfile frame;
frame.index = mNextFrame++;
frame.wallMs = clockMs(CLOCK_MONOTONIC) - mFrameWallStart;
frame.cpuMs = threadCpuMs() - mFrameCpuStart;
frame.sectionMs.assign(mSectionMs.begin(), mSectionMs.begin() + (ptrdiff_t) mSectionNames.size());
frame.sectionCalls.assign(mSectionCalls.begin(), mSectionCalls.begin() + (ptrdiff_t) mSectionNames.size());
frame.counters.assign(mCounters.begin(), mCounters.begin() + (ptrdiff_t) mCounterNames.size());
mFrames.push_back(std::move(frame));
}
void Profiler::addSectionTime(ProfileId section, double milliseconds) {
if (!mInFrame || section >= kMaxNames) {
return;
}
mSectionMs[section] += milliseconds;
mSectionCalls[section]++;
}
void Profiler::addCounter(ProfileId counter, double amount) {
if (!mInFrame || counter >= kMaxNames) {
return;
}
mCounters[counter] += amount;
}
void Profiler::clear() {
mFrames.clear();
mNextFrame = 0;
mInFrame = false;
}
std::vector<double> Profiler::sectionSeries(const std::string& name) const {
std::vector<double> series;
const ptrdiff_t index = indexOf(mSectionNames, name);
if (index < 0) {
return series;
}
for (const FrameProfile& frame : mFrames) {
series.push_back((size_t) index < frame.sectionMs.size() ? frame.sectionMs[(size_t) index] : 0.0);
}
return series;
}
std::vector<double> Profiler::counterSeries(const std::string& name) const {
std::vector<double> series;
const ptrdiff_t index = indexOf(mCounterNames, name);
if (index < 0) {
return series;
}
for (const FrameProfile& frame : mFrames) {
series.push_back((size_t) index < frame.counters.size() ? frame.counters[(size_t) index] : 0.0);
}
return series;
}
double Profiler::counterTotal(const std::string& name) const {
double total = 0;
for (double value : counterSeries(name)) {
total += value;
}
return total;
}
double Profiler::sectionTotal(const std::string& name) const {
double total = 0;
for (double value : sectionSeries(name)) {
total += value;
}
return total;
}
uint64_t Profiler::sectionCallTotal(const std::string& name) const {
uint64_t total = 0;
const ptrdiff_t index = indexOf(mSectionNames, name);
if (index < 0) {
return 0;
}
for (const FrameProfile& frame : mFrames) {
if ((size_t) index < frame.sectionCalls.size()) {
total += frame.sectionCalls[(size_t) index];
}
}
return total;
}
double Profiler::threadCpuMs() { return clockMs(CLOCK_THREAD_CPUTIME_ID); }
double Profiler::processCpuMs() { return clockMs(CLOCK_PROCESS_CPUTIME_ID); }
double Profiler::peakResidentMegabytes() {
rusage usage{};
getrusage(RUSAGE_SELF, &usage);
return (double) usage.ru_maxrss / 1024.0;
}
ProfileScope::ProfileScope(Profiler& profiler, ProfileId section) :
mProfiler(profiler),
mSection(section) {
mProfiler.trail().push(mProfiler.sectionName(section));
if (mProfiler.isInFrame()) {
mStartNs = monotonicNs();
}
}
ProfileScope::ProfileScope(Profiler& profiler, const char* sectionName) :
ProfileScope(profiler, profiler.section(sectionName)) {}
ProfileScope::~ProfileScope() {
mProfiler.trail().pop();
if (mStartNs < 0) {
return;
}
mProfiler.addSectionTime(mSection, (double) (monotonicNs() - mStartNs) / 1.0e6);
}
}

View file

@ -0,0 +1,59 @@
#include "UiTrail.hpp"
#include <algorithm>
namespace tp {
void UiTrail::push(const char* name) {
if (!isTracked()) {
return;
}
const int depth = mDepth.load(std::memory_order_relaxed);
if (depth < kDepth) {
mNames[depth].store(name, std::memory_order_relaxed);
} else {
mOverflow.fetch_add(1, std::memory_order_relaxed);
}
mDepth.store(depth + 1, std::memory_order_release);
}
void UiTrail::pop() {
if (!isTracked()) {
return;
}
const int depth = mDepth.load(std::memory_order_relaxed);
if (depth <= 0) {
return;
}
if (depth > kDepth) {
mOverflow.fetch_sub(1, std::memory_order_relaxed);
}
mDepth.store(depth - 1, std::memory_order_release);
}
bool UiTrail::isTracked() const {
return mTrackedThread.load(std::memory_order_relaxed) == std::this_thread::get_id();
}
void UiTrail::trackCallingThread() {
mDepth.store(0);
mOverflow.store(0);
mTrackedThread.store(std::this_thread::get_id());
}
void UiTrail::stopTracking() { mTrackedThread.store(std::thread::id{}); }
std::vector<std::string> UiTrail::snapshot() const {
const int depth = std::min(mDepth.load(std::memory_order_acquire), kDepth);
std::vector<std::string> names;
for (int index = 0; index < depth; index++) {
const char* name = mNames[index].load(std::memory_order_relaxed);
names.emplace_back(name ? name : "?");
}
const int overflow = mOverflow.load(std::memory_order_relaxed);
if (overflow > 0) {
names.push_back("... " + std::to_string(overflow) + " deeper");
}
return names;
}
}

View file

@ -0,0 +1,122 @@
#pragma once
#include "StackTrace.hpp"
#include "UiTrail.hpp"
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <filesystem>
#include <functional>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
namespace tp {
enum class UiPhase : uint8_t { Idle, Input, Draw, Present };
struct HangSettings {
double thresholdMs = 2000;
std::vector<double> followUpMs = { 10000, 30000 };
int stackSamples = 5;
double stackSampleIntervalMs = 200;
double processPauseMs = 1000;
size_t keptReports = 20;
std::string application = "app";
std::string build = "dev";
std::filesystem::path directory;
std::function<int64_t()> clockNs;
};
struct HangReportFile {
std::string fileName;
std::string text;
};
class HangWatchdog {
public:
using ReportFileSource = std::function<std::vector<HangReportFile>()>;
HangWatchdog(HangSettings settings, UiTrail& trail);
~HangWatchdog();
HangWatchdog(const HangWatchdog&) = delete;
HangWatchdog& operator=(const HangWatchdog&) = delete;
void enterPhase(UiPhase phase);
void noteInputEvents(uint64_t count = 1);
void addReportFileSource(ReportFileSource reportFileSource);
[[nodiscard]] const std::filesystem::path& directory() const { return mSettings.directory; }
[[nodiscard]] std::vector<std::filesystem::path> reports() const;
[[nodiscard]] size_t reportsWritten() const { return mReportsWritten.load(); }
[[nodiscard]] uint64_t inspections() const { return mInspections.load(); }
[[nodiscard]] static std::filesystem::path defaultHangsDirectory();
[[nodiscard]] static std::filesystem::path directoryFor(const std::filesystem::path& applicationHangsDirectory);
[[nodiscard]] static std::string processName();
static void allowDebuggerAttach();
[[nodiscard]] static const char* phaseName(UiPhase phase);
private:
struct Stall {
uint64_t beat = 0;
UiPhase phase = UiPhase::Idle;
std::filesystem::path report;
size_t followUps = 0;
};
struct Investigation {
std::string verdict;
std::string threads;
};
void watch();
void inspect(Stall& stall, int64_t now);
void discountProcessPause(int64_t now, int64_t previous);
[[nodiscard]] double stalledMs(int64_t now) const;
[[nodiscard]] uint64_t inputEventsInPhase() const;
void beginReport(Stall& stall, double stalled);
void appendFollowUp(Stall& stall, double stalled);
void finishRecovered(Stall& stall, double stalled);
[[nodiscard]] std::string reportHeader() const;
[[nodiscard]] std::string uiSnapshot(double stalled, UiPhase phase) const;
void writeSourcedFiles(const std::filesystem::path& report);
[[nodiscard]] Investigation investigate() const;
[[nodiscard]] std::string
sampleStacks(std::vector<std::vector<uintptr_t>>& uiSamples, std::string& uiState, std::string& uiWait) const;
[[nodiscard]] static std::string formatThreads(const std::vector<ThreadSample>& threads, int uiTid);
[[nodiscard]] static std::string stackVerdict(
const std::vector<std::vector<uintptr_t>>& uiSamples, const std::string& state, const std::string& waitChannel
);
void pruneOldReports() const;
[[nodiscard]] std::filesystem::path newReportPath() const;
[[nodiscard]] int64_t now() const;
HangSettings mSettings;
UiTrail& mTrail;
std::atomic<uint64_t> mBeat{ 0 };
std::atomic<int64_t> mPhaseStartNs{ 0 };
std::atomic<UiPhase> mPhase{ UiPhase::Idle };
std::atomic<uint64_t> mInputEvents{ 0 };
std::atomic<uint64_t> mPhaseInputEvents{ 0 };
std::atomic<size_t> mReportsWritten{ 0 };
std::atomic<uint64_t> mInspections{ 0 };
int mUiTid = 0;
uint64_t mPausedBeat = 0;
int64_t mPausedNs = 0;
std::mutex mReportFileSourcesMutex;
std::vector<ReportFileSource> mReportFileSources;
std::mutex mStopMutex;
std::condition_variable mStopped;
bool mStopping = false;
std::thread mThread;
};
}

View file

@ -0,0 +1,116 @@
#pragma once
#include "UiTrail.hpp"
#include <array>
#include <cstdint>
#include <deque>
#include <string>
#include <vector>
namespace tp {
using ProfileId = uint32_t;
struct FrameProfile {
uint64_t index = 0;
double wallMs = 0;
double cpuMs = 0;
std::vector<double> sectionMs;
std::vector<uint32_t> sectionCalls;
std::vector<double> counters;
};
struct ProfileStatistics {
size_t samples = 0;
double total = 0;
double mean = 0;
double minimum = 0;
double median = 0;
double p95 = 0;
double p99 = 0;
double maximum = 0;
};
[[nodiscard]] ProfileStatistics summarize(std::vector<double> values);
// A frame holds two generic channels: sections measure milliseconds, counters
// measure amounts. Nothing here knows which device or subsystem produced a
// number - a GPU timer reports into the section channel like any other sampler.
class Profiler {
public:
static constexpr size_t kMaxNames = 128;
void setEnabled(bool enabled);
[[nodiscard]] bool isEnabled() const { return mEnabled; }
[[nodiscard]] ProfileId section(const char* name);
[[nodiscard]] ProfileId counter(const char* name);
void beginFrame();
void endFrame();
[[nodiscard]] bool isInFrame() const { return mInFrame; }
// A duration measured outside the profiler (a GPU timer, a blocking wait, ...).
// ProfileScope does the same for a scoped block. Both count one sample.
void addSectionTime(ProfileId section, double milliseconds);
void addCounter(ProfileId counter, double amount);
void clear();
[[nodiscard]] const std::vector<FrameProfile>& frames() const { return mFrames; }
[[nodiscard]] const std::deque<std::string>& sectionNames() const { return mSectionNames; }
[[nodiscard]] const std::deque<std::string>& counterNames() const { return mCounterNames; }
[[nodiscard]] const char* sectionName(ProfileId section) const;
[[nodiscard]] std::vector<double> sectionSeries(const std::string& name) const;
[[nodiscard]] std::vector<double> counterSeries(const std::string& name) const;
[[nodiscard]] double counterTotal(const std::string& name) const;
[[nodiscard]] double sectionTotal(const std::string& name) const;
[[nodiscard]] uint64_t sectionCallTotal(const std::string& name) const;
// Open profile scopes of the frame loop, readable from a watchdog thread.
[[nodiscard]] UiTrail& trail() { return mTrail; }
[[nodiscard]] const UiTrail& trail() const { return mTrail; }
[[nodiscard]] static double threadCpuMs();
[[nodiscard]] static double processCpuMs();
[[nodiscard]] static double peakResidentMegabytes();
private:
static ProfileId intern(std::deque<std::string>& names, const char* name);
[[nodiscard]] static ptrdiff_t indexOf(const std::deque<std::string>& names, const std::string& name);
bool mEnabled = false;
bool mInFrame = false;
uint64_t mNextFrame = 0;
double mFrameWallStart = 0;
double mFrameCpuStart = 0;
std::deque<std::string> mSectionNames;
std::deque<std::string> mCounterNames;
std::array<double, kMaxNames> mSectionMs{};
std::array<uint32_t, kMaxNames> mSectionCalls{};
std::array<double, kMaxNames> mCounters{};
std::vector<FrameProfile> mFrames;
UiTrail mTrail;
};
// Times a block into one section and records it on the trail while it is open.
class ProfileScope {
public:
ProfileScope(Profiler& profiler, ProfileId section);
ProfileScope(Profiler& profiler, const char* sectionName);
~ProfileScope();
ProfileScope(const ProfileScope&) = delete;
ProfileScope& operator=(const ProfileScope&) = delete;
private:
Profiler& mProfiler;
ProfileId mSection;
int64_t mStartNs = -1;
};
}

View file

@ -0,0 +1,31 @@
#pragma once
#include <atomic>
#include <string>
#include <thread>
#include <vector>
namespace tp {
// The stack of named scopes open on the tracked thread; another thread may take a snapshot at any time.
class UiTrail {
public:
static constexpr int kDepth = 32;
void push(const char* name);
void pop();
void trackCallingThread();
void stopTracking();
[[nodiscard]] std::vector<std::string> snapshot() const;
private:
[[nodiscard]] bool isTracked() const;
std::atomic<const char*> mNames[kDepth] = {};
std::atomic<int> mDepth{ 0 };
std::atomic<int> mOverflow{ 0 };
std::atomic<std::thread::id> mTrackedThread{};
};
}

View file

@ -0,0 +1,226 @@
#include "GpuDevice.hpp"
#include "GpuError.hpp"
#include <algorithm>
#include <array>
#include <cstdlib>
#include <cstring>
namespace tp {
namespace {
constexpr std::array<const char*, 1> kDeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
constexpr const char* kValidationLayer = "VK_LAYER_KHRONOS_validation";
bool validationRequested() {
const char* value = std::getenv("MODULES_VULKAN_VALIDATION");
return value && std::strcmp(value, "1") == 0;
}
bool hasValidationLayer() {
uint32_t count = 0;
vkEnumerateInstanceLayerProperties(&count, nullptr);
std::vector<VkLayerProperties> layers(count);
vkEnumerateInstanceLayerProperties(&count, layers.data());
for (const VkLayerProperties& layer : layers) {
if (std::strcmp(layer.layerName, kValidationLayer) == 0) {
return true;
}
}
return false;
}
bool supportsDeviceExtensions(VkPhysicalDevice device) {
uint32_t count = 0;
vkEnumerateDeviceExtensionProperties(device, nullptr, &count, nullptr);
std::vector<VkExtensionProperties> available(count);
vkEnumerateDeviceExtensionProperties(device, nullptr, &count, available.data());
for (const char* required : kDeviceExtensions) {
const bool found =
std::any_of(available.begin(), available.end(), [required](const VkExtensionProperties& extension) {
return std::strcmp(required, extension.extensionName) == 0;
});
if (!found) {
return false;
}
}
return true;
}
uint32_t graphicsFamilyOf(VkPhysicalDevice device) {
uint32_t count = 0;
vkGetPhysicalDeviceQueueFamilyProperties(device, &count, nullptr);
std::vector<VkQueueFamilyProperties> families(count);
vkGetPhysicalDeviceQueueFamilyProperties(device, &count, families.data());
for (uint32_t index = 0; index < count; index++) {
if (families[index].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
return index;
}
}
return UINT32_MAX;
}
}
GpuDevice::GpuDevice(const std::vector<const char*>& surfaceExtensions) {
createInstance(surfaceExtensions);
selectPhysicalDevice();
createLogicalDevice();
createCommandPool();
}
GpuDevice::~GpuDevice() {
waitIdle();
if (mCommandPool != VK_NULL_HANDLE) {
vkDestroyCommandPool(mDevice, mCommandPool, nullptr);
}
if (mDevice != VK_NULL_HANDLE) {
vkDestroyDevice(mDevice, nullptr);
}
if (mInstance != VK_NULL_HANDLE) {
vkDestroyInstance(mInstance, nullptr);
}
}
void GpuDevice::createInstance(const std::vector<const char*>& surfaceExtensions) {
std::vector<const char*> layers;
if (validationRequested() && hasValidationLayer()) {
layers.push_back(kValidationLayer);
}
const VkApplicationInfo appInfo{
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
.pApplicationName = "Modules",
.apiVersion = VK_API_VERSION_1_2,
};
const VkInstanceCreateInfo createInfo{
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
.pApplicationInfo = &appInfo,
.enabledLayerCount = (uint32_t) layers.size(),
.ppEnabledLayerNames = layers.data(),
.enabledExtensionCount = (uint32_t) surfaceExtensions.size(),
.ppEnabledExtensionNames = surfaceExtensions.data(),
};
requireSuccess(vkCreateInstance(&createInfo, nullptr, &mInstance), "vkCreateInstance");
}
void GpuDevice::selectPhysicalDevice() {
uint32_t count = 0;
vkEnumeratePhysicalDevices(mInstance, &count, nullptr);
std::vector<VkPhysicalDevice> devices(count);
vkEnumeratePhysicalDevices(mInstance, &count, devices.data());
for (VkPhysicalDevice device : devices) {
const uint32_t graphics = graphicsFamilyOf(device);
if (graphics == UINT32_MAX || !supportsDeviceExtensions(device)) {
continue;
}
mPhysicalDevice = device;
mGraphicsQueueFamily = graphics;
vkGetPhysicalDeviceProperties(device, &mProperties);
return;
}
throw GpuError("selecting a graphics device with swapchain support", VK_ERROR_INITIALIZATION_FAILED);
}
void GpuDevice::createLogicalDevice() {
const float priority = 1.0f;
const VkDeviceQueueCreateInfo queueInfo{
.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = mGraphicsQueueFamily,
.queueCount = 1,
.pQueuePriorities = &priority,
};
VkPhysicalDeviceFeatures supported{};
vkGetPhysicalDeviceFeatures(mPhysicalDevice, &supported);
VkPhysicalDeviceFeatures features{};
features.geometryShader = supported.geometryShader;
const VkDeviceCreateInfo createInfo{
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
.queueCreateInfoCount = 1,
.pQueueCreateInfos = &queueInfo,
.enabledExtensionCount = (uint32_t) kDeviceExtensions.size(),
.ppEnabledExtensionNames = kDeviceExtensions.data(),
.pEnabledFeatures = &features,
};
requireSuccess(vkCreateDevice(mPhysicalDevice, &createInfo, nullptr, &mDevice), "vkCreateDevice");
vkGetDeviceQueue(mDevice, mGraphicsQueueFamily, 0, &mGraphicsQueue);
}
void GpuDevice::createCommandPool() {
const VkCommandPoolCreateInfo poolInfo{
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
.queueFamilyIndex = mGraphicsQueueFamily,
};
requireSuccess(vkCreateCommandPool(mDevice, &poolInfo, nullptr, &mCommandPool), "vkCreateCommandPool");
}
bool GpuDevice::canPresent(VkSurfaceKHR surface) const {
VkBool32 supported = VK_FALSE;
vkGetPhysicalDeviceSurfaceSupportKHR(mPhysicalDevice, mGraphicsQueueFamily, surface, &supported);
return supported == VK_TRUE;
}
uint32_t GpuDevice::findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) const {
VkPhysicalDeviceMemoryProperties memory{};
vkGetPhysicalDeviceMemoryProperties(mPhysicalDevice, &memory);
for (uint32_t index = 0; index < memory.memoryTypeCount; index++) {
const bool allowed = typeFilter & (1u << index);
if (allowed && (memory.memoryTypes[index].propertyFlags & properties) == properties) {
return index;
}
}
throw GpuError("finding a memory type", VK_ERROR_FEATURE_NOT_PRESENT);
}
VkCommandBuffer GpuDevice::beginOneShotCommands() const {
const VkCommandBufferAllocateInfo allocateInfo{
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
.commandPool = mCommandPool,
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
.commandBufferCount = 1,
};
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
requireSuccess(vkAllocateCommandBuffers(mDevice, &allocateInfo, &commandBuffer), "vkAllocateCommandBuffers");
const VkCommandBufferBeginInfo beginInfo{
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
};
requireSuccess(vkBeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer");
return commandBuffer;
}
// Waits on a dedicated fence rather than the whole queue, so in-flight window frames keep running.
void GpuDevice::endOneShotCommands(VkCommandBuffer commandBuffer) const {
requireSuccess(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer");
const VkFenceCreateInfo fenceInfo{ .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
VkFence fence = VK_NULL_HANDLE;
requireSuccess(vkCreateFence(mDevice, &fenceInfo, nullptr, &fence), "vkCreateFence");
const VkSubmitInfo submitInfo{
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.commandBufferCount = 1,
.pCommandBuffers = &commandBuffer,
};
const VkResult submitted = vkQueueSubmit(mGraphicsQueue, 1, &submitInfo, fence);
if (submitted == VK_SUCCESS) {
vkWaitForFences(mDevice, 1, &fence, VK_TRUE, UINT64_MAX);
}
vkDestroyFence(mDevice, fence, nullptr);
vkFreeCommandBuffers(mDevice, mCommandPool, 1, &commandBuffer);
requireSuccess(submitted, "vkQueueSubmit");
}
void GpuDevice::waitIdle() const {
if (mDevice != VK_NULL_HANDLE) {
vkDeviceWaitIdle(mDevice);
}
}
}

View file

@ -0,0 +1,96 @@
#include "GpuTimer.hpp"
#include <vector>
namespace tp {
GpuTimer::GpuTimer(
VkDevice device, VkPhysicalDevice physicalDevice, uint32_t queueFamily, Profiler& profiler, const char* name
) :
mDevice(device),
mProfiler(profiler),
mSection(profiler.section(name)),
mPending(kSlots, false) {
VkPhysicalDeviceProperties properties{};
vkGetPhysicalDeviceProperties(physicalDevice, &properties);
uint32_t familyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &familyCount, nullptr);
std::vector<VkQueueFamilyProperties> families(familyCount);
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &familyCount, families.data());
const uint32_t validBits = queueFamily < familyCount ? families[queueFamily].timestampValidBits : 0;
if (validBits == 0 || properties.limits.timestampPeriod <= 0.0f) {
return;
}
mTickNs = properties.limits.timestampPeriod;
mValidMask = validBits >= 64 ? ~0ull : ((1ull << validBits) - 1ull);
VkQueryPoolCreateInfo info{
.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO,
.queryType = VK_QUERY_TYPE_TIMESTAMP,
.queryCount = kSlots * 2,
};
if (vkCreateQueryPool(mDevice, &info, nullptr, &mPool) != VK_SUCCESS) {
mPool = VK_NULL_HANDLE;
}
}
GpuTimer::~GpuTimer() {
if (mPool != VK_NULL_HANDLE) {
vkDestroyQueryPool(mDevice, mPool, nullptr);
}
}
void GpuTimer::begin(VkCommandBuffer commandBuffer) {
mOpenSlot = UINT32_MAX;
if (!isSupported() || !mProfiler.isEnabled()) {
return;
}
collect();
const uint32_t slot = mNextSlot;
if (mPending[slot]) {
return;
}
vkCmdResetQueryPool(commandBuffer, mPool, slot * 2, 2);
vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, mPool, slot * 2);
mOpenSlot = slot;
}
void GpuTimer::end(VkCommandBuffer commandBuffer) {
if (mOpenSlot == UINT32_MAX) {
return;
}
vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, mPool, mOpenSlot * 2 + 1);
mPending[mOpenSlot] = true;
mNextSlot = (mOpenSlot + 1) % kSlots;
mOpenSlot = UINT32_MAX;
}
void GpuTimer::collect() {
if (!isSupported()) {
return;
}
for (uint32_t offset = 0; offset < kSlots; offset++) {
const uint32_t slot = (mNextSlot + offset) % kSlots;
if (!mPending[slot]) {
continue;
}
uint64_t stamps[2] = {};
const VkResult result = vkGetQueryPoolResults(
mDevice, mPool, slot * 2, 2, sizeof(stamps), stamps, sizeof(uint64_t), VK_QUERY_RESULT_64_BIT
);
if (result != VK_SUCCESS) {
continue;
}
mPending[slot] = false;
const uint64_t ticks = (stamps[1] - stamps[0]) & mValidMask;
mProfiler.addSectionTime(mSection, (double) ticks * mTickNs / 1.0e6);
}
}
}

View file

@ -0,0 +1,43 @@
#pragma once
#include "Profiler.hpp"
#include <vulkan/vulkan.h>
#include <cstdint>
#include <vector>
namespace tp {
// Measures GPU time of recorded command ranges with timestamp queries and reports it as a profiler section.
class GpuTimer {
public:
static constexpr uint32_t kSlots = 8;
GpuTimer(
VkDevice device, VkPhysicalDevice physicalDevice, uint32_t queueFamily, Profiler& profiler, const char* name
);
~GpuTimer();
GpuTimer(const GpuTimer&) = delete;
GpuTimer& operator=(const GpuTimer&) = delete;
void begin(VkCommandBuffer commandBuffer);
void end(VkCommandBuffer commandBuffer);
private:
[[nodiscard]] bool isSupported() const { return mPool != VK_NULL_HANDLE; }
void collect();
VkDevice mDevice;
Profiler& mProfiler;
ProfileId mSection;
VkQueryPool mPool = VK_NULL_HANDLE;
double mTickNs = 1;
uint64_t mValidMask = ~0ull;
std::vector<bool> mPending;
uint32_t mNextSlot = 0;
uint32_t mOpenSlot = UINT32_MAX;
};
}

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,571 @@
#include "WindowSurface.hpp"
#include "GpuError.hpp"
#include "GpuTimer.hpp"
#include "Profiler.hpp"
#include <algorithm>
#include <array>
#include <limits>
namespace tp {
namespace {
constexpr uint32_t kUndefinedExtent = std::numeric_limits<uint32_t>::max();
VkExtent2D atLeastOnePixel(VkExtent2D extent) {
return { std::max(extent.width, 1u), std::max(extent.height, 1u) };
}
bool isEmpty(VkExtent2D extent) { return extent.width == 0 || extent.height == 0; }
VkSurfaceCapabilitiesKHR capabilitiesOf(VkPhysicalDevice device, VkSurfaceKHR surface) {
VkSurfaceCapabilitiesKHR capabilities{};
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &capabilities);
return capabilities;
}
}
WindowSurface::WindowSurface(GpuDevice& gpu, VulkanSurfaceSource& source, Profiler& profiler) :
mGpu(gpu),
mDevice(gpu.device()),
mSource(source),
mProfiler(profiler),
mAcquireSection(profiler.section("frame.acquire")),
mSubmitSection(profiler.section("frame.submit")) {
createSurface();
selectCompositeAlpha();
createRenderPass();
createFrameSlots();
mFrameTimer =
std::make_unique<GpuTimer>(mDevice, gpu.physicalDevice(), gpu.graphicsQueueFamily(), profiler, "gpu.window");
const VkExtent2D extent = mSource.framebufferExtent();
createSwapchain(atLeastOnePixel(extent));
mRequestedExtent = extent;
}
WindowSurface::~WindowSurface() {
mGpu.waitIdle();
mFrameTimer.reset();
destroySwapchain();
destroyFrameSlots();
if (mRenderPass != VK_NULL_HANDLE) {
vkDestroyRenderPass(mDevice, mRenderPass, nullptr);
}
if (mSurface != VK_NULL_HANDLE) {
vkDestroySurfaceKHR(mGpu.instance(), mSurface, nullptr);
}
}
void WindowSurface::createSurface() {
mSurface = mSource.createSurface(mGpu.instance());
if (mSurface == VK_NULL_HANDLE) {
throw GpuError("creating the window surface", VK_ERROR_SURFACE_LOST_KHR);
}
if (!mGpu.canPresent(mSurface)) {
throw GpuError("presenting to the window surface from the graphics queue", VK_ERROR_INCOMPATIBLE_DISPLAY_KHR);
}
}
void WindowSurface::selectCompositeAlpha() {
const VkCompositeAlphaFlagsKHR supported = capabilitiesOf(mGpu.physicalDevice(), mSurface).supportedCompositeAlpha;
mCompositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
if (!(supported & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)) {
for (VkCompositeAlphaFlagBitsKHR candidate : { VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR,
VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR,
VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR }) {
if (supported & candidate) {
mCompositeAlpha = candidate;
break;
}
}
}
if (mSource.wantsTranslucency() && (supported & VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR)) {
mCompositeAlpha = VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR;
mTranslucent = true;
}
mSource.onTranslucencyResolved(mTranslucent);
}
void WindowSurface::createRenderPass() {
uint32_t formatCount = 0;
vkGetPhysicalDeviceSurfaceFormatsKHR(mGpu.physicalDevice(), mSurface, &formatCount, nullptr);
std::vector<VkSurfaceFormatKHR> formats(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(mGpu.physicalDevice(), mSurface, &formatCount, formats.data());
if (formats.empty()) {
throw GpuError("querying surface formats", VK_ERROR_FORMAT_NOT_SUPPORTED);
}
mSwapchainFormat = formats.front().format;
for (const VkSurfaceFormatKHR& format : formats) {
if (format.format == VK_FORMAT_B8G8R8A8_UNORM && format.colorSpace == VK_COLORSPACE_SRGB_NONLINEAR_KHR) {
mSwapchainFormat = format.format;
break;
}
}
mDepthStencilFormat = selectDepthStencilFormat();
const std::array<VkAttachmentDescription, 2> attachments = {
VkAttachmentDescription{
.format = mSwapchainFormat,
.samples = VK_SAMPLE_COUNT_1_BIT,
.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
.storeOp = VK_ATTACHMENT_STORE_OP_STORE,
.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
},
VkAttachmentDescription{
.format = mDepthStencilFormat,
.samples = VK_SAMPLE_COUNT_1_BIT,
.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
},
};
const VkAttachmentReference colorReference{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
const VkAttachmentReference depthReference{ 1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL };
const VkSubpassDescription subpass{
.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
.colorAttachmentCount = 1,
.pColorAttachments = &colorReference,
.pDepthStencilAttachment = &depthReference,
};
const VkSubpassDependency dependency{
.srcSubpass = VK_SUBPASS_EXTERNAL,
.dstSubpass = 0,
.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT,
.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT,
.srcAccessMask = 0,
.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
};
const VkRenderPassCreateInfo createInfo{
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
.attachmentCount = (uint32_t) attachments.size(),
.pAttachments = attachments.data(),
.subpassCount = 1,
.pSubpasses = &subpass,
.dependencyCount = 1,
.pDependencies = &dependency,
};
requireSuccess(vkCreateRenderPass(mDevice, &createInfo, nullptr, &mRenderPass), "vkCreateRenderPass");
}
VkFormat WindowSurface::selectDepthStencilFormat() const {
for (VkFormat format : { VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D16_UNORM_S8_UINT }) {
VkFormatProperties properties{};
vkGetPhysicalDeviceFormatProperties(mGpu.physicalDevice(), format, &properties);
if (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) {
return format;
}
}
throw GpuError("selecting a depth-stencil format", VK_ERROR_FORMAT_NOT_SUPPORTED);
}
void WindowSurface::createFrameSlots() {
VkCommandBuffer commandBuffers[kFramesInFlight] = {};
const VkCommandBufferAllocateInfo allocateInfo{
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
.commandPool = mGpu.commandPool(),
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
.commandBufferCount = kFramesInFlight,
};
requireSuccess(vkAllocateCommandBuffers(mDevice, &allocateInfo, commandBuffers), "vkAllocateCommandBuffers");
const VkSemaphoreCreateInfo semaphoreInfo{ .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
const VkFenceCreateInfo fenceInfo{
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
.flags = VK_FENCE_CREATE_SIGNALED_BIT,
};
for (uint32_t index = 0; index < kFramesInFlight; index++) {
FrameSlot& slot = mFrameSlots[index];
slot.commandBuffer = commandBuffers[index];
requireSuccess(vkCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &slot.imageAvailable), "vkCreateSemaphore");
requireSuccess(vkCreateFence(mDevice, &fenceInfo, nullptr, &slot.inFlight), "vkCreateFence");
}
}
void WindowSurface::destroyFrameSlots() {
for (FrameSlot& slot : mFrameSlots) {
if (slot.imageAvailable != VK_NULL_HANDLE) {
vkDestroySemaphore(mDevice, slot.imageAvailable, nullptr);
}
if (slot.inFlight != VK_NULL_HANDLE) {
vkDestroyFence(mDevice, slot.inFlight, nullptr);
}
if (slot.commandBuffer != VK_NULL_HANDLE) {
vkFreeCommandBuffers(mDevice, mGpu.commandPool(), 1, &slot.commandBuffer);
}
slot = {};
}
}
// Mailbox replaces the queued frame instead of waiting behind it; FIFO is the guaranteed fallback.
VkPresentModeKHR WindowSurface::selectPresentMode() const {
uint32_t count = 0;
vkGetPhysicalDeviceSurfacePresentModesKHR(mGpu.physicalDevice(), mSurface, &count, nullptr);
std::vector<VkPresentModeKHR> modes(count);
vkGetPhysicalDeviceSurfacePresentModesKHR(mGpu.physicalDevice(), mSurface, &count, modes.data());
const bool hasMailbox = std::find(modes.begin(), modes.end(), VK_PRESENT_MODE_MAILBOX_KHR) != modes.end();
return hasMailbox ? VK_PRESENT_MODE_MAILBOX_KHR : VK_PRESENT_MODE_FIFO_KHR;
}
void WindowSurface::createSwapchain(VkExtent2D requested, VkSwapchainKHR previous) {
const VkSurfaceCapabilitiesKHR capabilities = capabilitiesOf(mGpu.physicalDevice(), mSurface);
mSwapchainExtent = capabilities.currentExtent;
mSurfaceTransform = capabilities.currentTransform;
mSurfaceExtent = capabilities.currentExtent;
VkSurfaceTransformFlagBitsKHR transform = capabilities.currentTransform;
const bool canRenderUnrotated = capabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
if (transform != VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR && canRenderUnrotated) {
transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
const bool quarterTurn = capabilities.currentTransform &
(VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR | VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR);
if (quarterTurn && mSwapchainExtent.width != kUndefinedExtent) {
std::swap(mSwapchainExtent.width, mSwapchainExtent.height);
}
}
if (mSwapchainExtent.width == kUndefinedExtent) {
mSwapchainExtent.width =
std::clamp(requested.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width);
mSwapchainExtent.height =
std::clamp(requested.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height);
}
const VkPresentModeKHR presentMode = selectPresentMode();
uint32_t imageCount = capabilities.minImageCount + (presentMode == VK_PRESENT_MODE_MAILBOX_KHR ? 1 : 0);
if (capabilities.maxImageCount > 0) {
imageCount = std::min(imageCount, capabilities.maxImageCount);
}
const VkSwapchainCreateInfoKHR createInfo{
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
.surface = mSurface,
.minImageCount = imageCount,
.imageFormat = mSwapchainFormat,
.imageColorSpace = VK_COLORSPACE_SRGB_NONLINEAR_KHR,
.imageExtent = mSwapchainExtent,
.imageArrayLayers = 1,
.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE,
.preTransform = transform,
.compositeAlpha = mCompositeAlpha,
.presentMode = presentMode,
.clipped = VK_TRUE,
.oldSwapchain = previous,
};
const VkResult created = vkCreateSwapchainKHR(mDevice, &createInfo, nullptr, &mSwapchain);
if (previous != VK_NULL_HANDLE) {
vkDestroySwapchainKHR(mDevice, previous, nullptr);
}
requireSuccess(created, "vkCreateSwapchainKHR");
createSwapchainImages();
}
// Each image owns its render-finished semaphore: presentation may still hold it after its frame slot is reused.
void WindowSurface::createSwapchainImages() {
uint32_t count = 0;
vkGetSwapchainImagesKHR(mDevice, mSwapchain, &count, nullptr);
mSwapchainImages.resize(count);
vkGetSwapchainImagesKHR(mDevice, mSwapchain, &count, mSwapchainImages.data());
createDepthStencil();
const VkSemaphoreCreateInfo semaphoreInfo{ .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
mSwapchainImageViews.resize(count);
mSwapchainFramebuffers.resize(count);
mRenderFinishedSemaphores.resize(count);
for (uint32_t index = 0; index < count; index++) {
const VkImageViewCreateInfo viewInfo{
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.image = mSwapchainImages[index],
.viewType = VK_IMAGE_VIEW_TYPE_2D,
.format = mSwapchainFormat,
.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 },
};
requireSuccess(vkCreateImageView(mDevice, &viewInfo, nullptr, &mSwapchainImageViews[index]), "vkCreateImageView");
const std::array<VkImageView, 2> attachments = { mSwapchainImageViews[index], mDepthStencilView };
const VkFramebufferCreateInfo framebufferInfo{
.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
.renderPass = mRenderPass,
.attachmentCount = (uint32_t) attachments.size(),
.pAttachments = attachments.data(),
.width = mSwapchainExtent.width,
.height = mSwapchainExtent.height,
.layers = 1,
};
requireSuccess(
vkCreateFramebuffer(mDevice, &framebufferInfo, nullptr, &mSwapchainFramebuffers[index]), "vkCreateFramebuffer"
);
requireSuccess(
vkCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &mRenderFinishedSemaphores[index]), "vkCreateSemaphore"
);
}
}
void WindowSurface::createDepthStencil() {
const VkImageCreateInfo imageInfo{
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
.imageType = VK_IMAGE_TYPE_2D,
.format = mDepthStencilFormat,
.extent = { mSwapchainExtent.width, mSwapchainExtent.height, 1 },
.mipLevels = 1,
.arrayLayers = 1,
.samples = VK_SAMPLE_COUNT_1_BIT,
.tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
};
requireSuccess(vkCreateImage(mDevice, &imageInfo, nullptr, &mDepthStencilImage), "vkCreateImage");
VkMemoryRequirements requirements{};
vkGetImageMemoryRequirements(mDevice, mDepthStencilImage, &requirements);
const VkMemoryAllocateInfo allocateInfo{
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
.allocationSize = requirements.size,
.memoryTypeIndex = mGpu.findMemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT),
};
requireSuccess(vkAllocateMemory(mDevice, &allocateInfo, nullptr, &mDepthStencilMemory), "vkAllocateMemory");
requireSuccess(vkBindImageMemory(mDevice, mDepthStencilImage, mDepthStencilMemory, 0), "vkBindImageMemory");
const VkImageViewCreateInfo viewInfo{
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.image = mDepthStencilImage,
.viewType = VK_IMAGE_VIEW_TYPE_2D,
.format = mDepthStencilFormat,
.subresourceRange = { VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT, 0, 1, 0, 1 },
};
requireSuccess(vkCreateImageView(mDevice, &viewInfo, nullptr, &mDepthStencilView), "vkCreateImageView");
}
void WindowSurface::destroyDepthStencil() {
if (mDepthStencilView != VK_NULL_HANDLE) {
vkDestroyImageView(mDevice, mDepthStencilView, nullptr);
}
if (mDepthStencilImage != VK_NULL_HANDLE) {
vkDestroyImage(mDevice, mDepthStencilImage, nullptr);
}
if (mDepthStencilMemory != VK_NULL_HANDLE) {
vkFreeMemory(mDevice, mDepthStencilMemory, nullptr);
}
mDepthStencilView = VK_NULL_HANDLE;
mDepthStencilImage = VK_NULL_HANDLE;
mDepthStencilMemory = VK_NULL_HANDLE;
}
void WindowSurface::destroySwapchainResources() {
destroyDepthStencil();
for (VkFramebuffer framebuffer : mSwapchainFramebuffers) {
vkDestroyFramebuffer(mDevice, framebuffer, nullptr);
}
for (VkImageView view : mSwapchainImageViews) {
vkDestroyImageView(mDevice, view, nullptr);
}
for (VkSemaphore semaphore : mRenderFinishedSemaphores) {
vkDestroySemaphore(mDevice, semaphore, nullptr);
}
mSwapchainFramebuffers.clear();
mSwapchainImageViews.clear();
mRenderFinishedSemaphores.clear();
mSwapchainImages.clear();
}
void WindowSurface::destroySwapchain() {
destroySwapchainResources();
if (mSwapchain != VK_NULL_HANDLE) {
vkDestroySwapchainKHR(mDevice, mSwapchain, nullptr);
}
mSwapchain = VK_NULL_HANDLE;
}
void WindowSurface::waitForFramesInFlight() {
VkFence fences[kFramesInFlight] = {};
for (uint32_t index = 0; index < kFramesInFlight; index++) {
fences[index] = mFrameSlots[index].inFlight;
}
vkWaitForFences(mDevice, kFramesInFlight, fences, VK_TRUE, UINT64_MAX);
}
// Only frames using the old views must finish; the old swapchain is handed over so presentation has no gap.
void WindowSurface::recreateSwapchain(VkExtent2D requested) {
waitForFramesInFlight();
vkQueueWaitIdle(mGpu.graphicsQueue());
destroySwapchainResources();
VkSwapchainKHR previous = mSwapchain;
mSwapchain = VK_NULL_HANDLE;
createSwapchain(atLeastOnePixel(requested), previous);
mRequestedExtent = requested;
mSwapchainRecreations++;
}
bool WindowSurface::swapchainMatches(VkExtent2D extent) const {
return mRequestedExtent.width == extent.width && mRequestedExtent.height == extent.height;
}
bool WindowSurface::surfaceChanged() const {
VkSurfaceCapabilitiesKHR capabilities{};
if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(mGpu.physicalDevice(), mSurface, &capabilities) != VK_SUCCESS) {
return true;
}
return capabilities.currentTransform != mSurfaceTransform ||
capabilities.currentExtent.width != mSurfaceExtent.width ||
capabilities.currentExtent.height != mSurfaceExtent.height;
}
std::optional<GpuFrame> WindowSurface::beginFrame() {
const VkExtent2D extent = mSource.framebufferExtent();
if (isEmpty(extent) || mSurface == VK_NULL_HANDLE) {
return std::nullopt;
}
if (!swapchainMatches(extent)) {
recreateSwapchain(extent);
}
FrameSlot& slot = mFrameSlots[mCurrentSlot];
VkResult acquired = VK_SUCCESS;
{
const ProfileScope scope(mProfiler, mAcquireSection);
vkWaitForFences(mDevice, 1, &slot.inFlight, VK_TRUE, UINT64_MAX);
acquired =
vkAcquireNextImageKHR(mDevice, mSwapchain, UINT64_MAX, slot.imageAvailable, VK_NULL_HANDLE, &mAcquiredImage);
}
if (acquired == VK_ERROR_OUT_OF_DATE_KHR) {
recreateSwapchain(extent);
return std::nullopt;
}
if (acquired != VK_SUBOPTIMAL_KHR) {
requireSuccess(acquired, "vkAcquireNextImageKHR");
}
requireSuccess(vkResetFences(mDevice, 1, &slot.inFlight), "vkResetFences");
recordFrameStart(slot.commandBuffer, mAcquiredImage);
mFrameActive = true;
return GpuFrame{ slot.commandBuffer, mCurrentSlot, mSwapchainExtent };
}
void WindowSurface::recordFrameStart(VkCommandBuffer commandBuffer, uint32_t imageIndex) {
requireSuccess(vkResetCommandBuffer(commandBuffer, 0), "vkResetCommandBuffer");
const VkCommandBufferBeginInfo beginInfo{ .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
requireSuccess(vkBeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer");
mFrameTimer->begin(commandBuffer);
std::array<VkClearValue, 2> clearValues{};
clearValues[0].color = { { 0.0f, 0.0f, 0.0f, mTranslucent ? 0.0f : 1.0f } };
clearValues[1].depthStencil = { 1.0f, 0 };
const VkRenderPassBeginInfo renderPassInfo{
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
.renderPass = mRenderPass,
.framebuffer = mSwapchainFramebuffers[imageIndex],
.renderArea = { .offset = { 0, 0 }, .extent = mSwapchainExtent },
.clearValueCount = (uint32_t) clearValues.size(),
.pClearValues = clearValues.data(),
};
vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
const VkViewport viewport{
.x = 0.0f,
.y = 0.0f,
.width = (float) mSwapchainExtent.width,
.height = (float) mSwapchainExtent.height,
.minDepth = 0.0f,
.maxDepth = 1.0f,
};
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
const VkRect2D scissor{ { 0, 0 }, mSwapchainExtent };
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
}
void WindowSurface::endFrame() {
if (!mFrameActive) {
return;
}
mFrameActive = false;
const ProfileScope scope(mProfiler, mSubmitSection);
FrameSlot& slot = mFrameSlots[mCurrentSlot];
vkCmdEndRenderPass(slot.commandBuffer);
mFrameTimer->end(slot.commandBuffer);
requireSuccess(vkEndCommandBuffer(slot.commandBuffer), "vkEndCommandBuffer");
VkSemaphore renderFinished = mRenderFinishedSemaphores[mAcquiredImage];
const VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
const VkSubmitInfo submitInfo{
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.waitSemaphoreCount = 1,
.pWaitSemaphores = &slot.imageAvailable,
.pWaitDstStageMask = &waitStage,
.commandBufferCount = 1,
.pCommandBuffers = &slot.commandBuffer,
.signalSemaphoreCount = 1,
.pSignalSemaphores = &renderFinished,
};
requireSuccess(vkQueueSubmit(mGpu.graphicsQueue(), 1, &submitInfo, slot.inFlight), "vkQueueSubmit");
const VkPresentInfoKHR presentInfo{
.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
.waitSemaphoreCount = 1,
.pWaitSemaphores = &renderFinished,
.swapchainCount = 1,
.pSwapchains = &mSwapchain,
.pImageIndices = &mAcquiredImage,
};
const VkResult presented = vkQueuePresentKHR(mGpu.graphicsQueue(), &presentInfo);
mCurrentSlot = (mCurrentSlot + 1) % kFramesInFlight;
const bool outOfDate = presented == VK_ERROR_OUT_OF_DATE_KHR;
if (outOfDate || (presented == VK_SUBOPTIMAL_KHR && surfaceChanged())) {
recreateSwapchain(mSource.framebufferExtent());
return;
}
if (presented != VK_SUBOPTIMAL_KHR) {
requireSuccess(presented, "vkQueuePresentKHR");
}
}
void WindowSurface::syncToSource() {
const VkExtent2D extent = mSource.framebufferExtent();
if (isEmpty(extent) || mSurface == VK_NULL_HANDLE) {
return;
}
recreateSwapchain(extent);
}
void WindowSurface::releaseSurface() {
if (mSurface == VK_NULL_HANDLE) {
return;
}
mGpu.waitIdle();
destroySwapchain();
mSwapchainExtent = { 0, 0 };
vkDestroySurfaceKHR(mGpu.instance(), mSurface, nullptr);
mSurface = VK_NULL_HANDLE;
}
void WindowSurface::recreateSurface() {
if (mSurface != VK_NULL_HANDLE) {
return;
}
createSurface();
const VkExtent2D extent = mSource.framebufferExtent();
createSwapchain(atLeastOnePixel(extent));
mRequestedExtent = extent;
}
}

View file

@ -0,0 +1,53 @@
#pragma once
#include "RenderTargetRegistry.hpp"
#include <vulkan/vulkan.h>
#include <vector>
namespace tp {
// The Vulkan instance, device and graphics queue shared by every window of the process.
class GpuDevice {
public:
explicit GpuDevice(const std::vector<const char*>& surfaceExtensions);
~GpuDevice();
GpuDevice(const GpuDevice&) = delete;
GpuDevice& operator=(const GpuDevice&) = delete;
[[nodiscard]] VkInstance instance() const { return mInstance; }
[[nodiscard]] VkPhysicalDevice physicalDevice() const { return mPhysicalDevice; }
[[nodiscard]] VkDevice device() const { return mDevice; }
[[nodiscard]] VkQueue graphicsQueue() const { return mGraphicsQueue; }
[[nodiscard]] uint32_t graphicsQueueFamily() const { return mGraphicsQueueFamily; }
[[nodiscard]] VkCommandPool commandPool() const { return mCommandPool; }
[[nodiscard]] const char* deviceName() const { return mProperties.deviceName; }
[[nodiscard]] bool canPresent(VkSurfaceKHR surface) const;
[[nodiscard]] uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) const;
[[nodiscard]] VkCommandBuffer beginOneShotCommands() const;
void endOneShotCommands(VkCommandBuffer commandBuffer) const;
void waitIdle() const;
[[nodiscard]] RenderTargetRegistry& renderTargets() { return mRenderTargets; }
private:
void createInstance(const std::vector<const char*>& surfaceExtensions);
void selectPhysicalDevice();
void createLogicalDevice();
void createCommandPool();
VkInstance mInstance = VK_NULL_HANDLE;
VkPhysicalDevice mPhysicalDevice = VK_NULL_HANDLE;
VkPhysicalDeviceProperties mProperties{};
VkDevice mDevice = VK_NULL_HANDLE;
uint32_t mGraphicsQueueFamily = UINT32_MAX;
VkQueue mGraphicsQueue = VK_NULL_HANDLE;
VkCommandPool mCommandPool = VK_NULL_HANDLE;
RenderTargetRegistry mRenderTargets;
};
}

View file

@ -0,0 +1,28 @@
#pragma once
#include <vulkan/vulkan.h>
#include <stdexcept>
#include <string>
namespace tp {
// A Vulkan call failed in a way the renderer cannot recover from.
class GpuError : public std::runtime_error {
public:
GpuError(const char* operation, VkResult result) :
std::runtime_error(std::string(operation) + " failed: VkResult " + std::to_string((int) result)),
mResult(result) {}
[[nodiscard]] VkResult result() const { return mResult; }
private:
VkResult mResult;
};
inline void requireSuccess(VkResult result, const char* operation) {
if (result != VK_SUCCESS) {
throw GpuError(operation, result);
}
}
}

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,125 @@
#pragma once
#include "GpuDevice.hpp"
#include <vulkan/vulkan.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <vector>
namespace tp {
class GpuTimer;
class Profiler;
// The native window a WindowSurface presents into.
class VulkanSurfaceSource {
public:
virtual ~VulkanSurfaceSource() = default;
[[nodiscard]] virtual VkSurfaceKHR createSurface(VkInstance instance) const = 0;
[[nodiscard]] virtual VkExtent2D framebufferExtent() const = 0;
[[nodiscard]] virtual bool wantsTranslucency() const = 0;
virtual void onTranslucencyResolved(bool translucent) = 0;
};
// The frame being recorded: renderers record into commandBuffer and key per-frame resources by slot.
struct GpuFrame {
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
uint32_t slot = 0;
VkExtent2D extent{};
};
// Presentation of one window: surface, swapchain, render pass and frame synchronisation.
class WindowSurface {
public:
static constexpr uint32_t kFramesInFlight = 2;
WindowSurface(GpuDevice& gpu, VulkanSurfaceSource& source, Profiler& profiler);
~WindowSurface();
WindowSurface(const WindowSurface&) = delete;
WindowSurface& operator=(const WindowSurface&) = delete;
[[nodiscard]] GpuDevice& gpu() const { return mGpu; }
[[nodiscard]] VkRenderPass renderPass() const { return mRenderPass; }
[[nodiscard]] uint32_t imageCount() const { return (uint32_t) mSwapchainImages.size(); }
[[nodiscard]] bool isTranslucent() const { return mTranslucent; }
[[nodiscard]] bool hasSurface() const { return mSurface != VK_NULL_HANDLE; }
[[nodiscard]] uint64_t swapchainRecreations() const { return mSwapchainRecreations; }
[[nodiscard]] std::optional<GpuFrame> beginFrame();
void endFrame();
void syncToSource();
void releaseSurface();
void recreateSurface();
private:
struct FrameSlot {
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
VkSemaphore imageAvailable = VK_NULL_HANDLE;
VkFence inFlight = VK_NULL_HANDLE;
};
void createSurface();
void selectCompositeAlpha();
void createRenderPass();
void createFrameSlots();
void destroyFrameSlots();
[[nodiscard]] VkPresentModeKHR selectPresentMode() const;
[[nodiscard]] VkFormat selectDepthStencilFormat() const;
[[nodiscard]] bool surfaceChanged() const;
[[nodiscard]] bool swapchainMatches(VkExtent2D extent) const;
void createSwapchain(VkExtent2D requested, VkSwapchainKHR previous = VK_NULL_HANDLE);
void createSwapchainImages();
void createDepthStencil();
void destroyDepthStencil();
void destroySwapchainResources();
void destroySwapchain();
void recreateSwapchain(VkExtent2D requested);
void waitForFramesInFlight();
void recordFrameStart(VkCommandBuffer commandBuffer, uint32_t imageIndex);
GpuDevice& mGpu;
VkDevice mDevice;
VulkanSurfaceSource& mSource;
Profiler& mProfiler;
VkSurfaceKHR mSurface = VK_NULL_HANDLE;
VkCompositeAlphaFlagBitsKHR mCompositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
bool mTranslucent = false;
VkSurfaceTransformFlagBitsKHR mSurfaceTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
VkExtent2D mSurfaceExtent{ 0, 0 };
VkExtent2D mRequestedExtent{ 0, 0 };
VkFormat mSwapchainFormat{};
VkFormat mDepthStencilFormat{};
VkRenderPass mRenderPass = VK_NULL_HANDLE;
VkSwapchainKHR mSwapchain = VK_NULL_HANDLE;
VkExtent2D mSwapchainExtent{};
std::vector<VkImage> mSwapchainImages;
std::vector<VkImageView> mSwapchainImageViews;
std::vector<VkFramebuffer> mSwapchainFramebuffers;
std::vector<VkSemaphore> mRenderFinishedSemaphores;
uint64_t mSwapchainRecreations = 0;
VkImage mDepthStencilImage = VK_NULL_HANDLE;
VkDeviceMemory mDepthStencilMemory = VK_NULL_HANDLE;
VkImageView mDepthStencilView = VK_NULL_HANDLE;
FrameSlot mFrameSlots[kFramesInFlight];
uint32_t mCurrentSlot = 0;
uint32_t mAcquiredImage = 0;
bool mFrameActive = false;
std::unique_ptr<GpuTimer> mFrameTimer;
uint32_t mAcquireSection = 0;
uint32_t mSubmitSection = 0;
};
}

View file

@ -0,0 +1,85 @@
#include "InputCoalescer.hpp"
using namespace tp;
namespace {
bool isGestureUpdate(const InputEvent& event) {
const auto* changed = std::get_if<GestureChanged>(&event);
return changed && changed->gesture.phase == GesturePhase::Update;
}
}
void InputCoalescer::post(const InputEvent& event) {
if (mTap) {
mTap->post(event);
}
mStatistics.received++;
if (const auto* sampled = std::get_if<PointerSampled>(&event)) {
mSample = sampled->sample;
return;
}
if (merge(event)) {
return;
}
mEvents.push_back({ event, mSample });
}
bool InputCoalescer::merge(const InputEvent& event) {
if (mEvents.empty()) {
return false;
}
Pending& previous = mEvents.back();
if (const auto* moved = std::get_if<PointerMoved>(&event)) {
auto* last = std::get_if<PointerMoved>(&previous.event);
if (!last || mPolicies.pointerMotion == DeliveryPolicy::Every) {
return false;
}
last->position =
mPolicies.pointerMotion == DeliveryPolicy::Latest ? moved->position : last->position + moved->position;
previous.sample = mSample;
return true;
}
if (const auto* scrolled = std::get_if<Scrolled>(&event)) {
auto* last = std::get_if<Scrolled>(&previous.event);
if (!last || last->source != scrolled->source || mPolicies.scroll == DeliveryPolicy::Every) {
return false;
}
last->delta = mPolicies.scroll == DeliveryPolicy::Latest ? scrolled->delta : last->delta + scrolled->delta;
return true;
}
if (isGestureUpdate(event) && isGestureUpdate(previous.event)) {
accumulate(std::get<GestureChanged>(previous.event).gesture, std::get<GestureChanged>(event).gesture);
return true;
}
return false;
}
bool InputCoalescer::carriesSample(const InputEvent& event) {
return std::holds_alternative<PointerMoved>(event) || std::holds_alternative<PointerButtonChanged>(event);
}
bool InputCoalescer::isEmptyScroll(const InputEvent& event) {
const auto* scrolled = std::get_if<Scrolled>(&event);
return scrolled && scrolled->delta == Vec2F{ 0, 0 };
}
void InputCoalescer::deliver(InputSink& sink) {
std::vector<Pending> events;
events.swap(mEvents);
for (const Pending& pending : events) {
if (isEmptyScroll(pending.event)) {
continue;
}
if (carriesSample(pending.event) && !(pending.sample == mDeliveredSample)) {
mDeliveredSample = pending.sample;
sink.post(PointerSampled{ pending.sample });
}
mStatistics.delivered++;
sink.post(pending.event);
}
}

View file

@ -0,0 +1,413 @@
#include "InputRecording.hpp"
#include <chrono>
#include <cstdio>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <utility>
namespace tp {
namespace {
const char* const kDeviceNames[] = { "mouse", "finger", "stylus" };
const char* const kTipNames[] = { "pen", "eraser", "brush", "pencil", "airbrush" };
const char* const kButtonNames[] = { "left", "right", "middle", "back", "forward" };
const char* const kScrollSourceNames[] = { "wheel", "touchpad" };
const char* const kKeyActionNames[] = { "press", "release", "repeat" };
const char* const kGesturePhaseNames[] = { "begin", "update", "end" };
template <typename Enum, size_t Count>
void writeName(std::ostream& out, const char* const (&names)[Count], Enum value) {
out << ' ' << names[(size_t) value];
}
template <typename Enum, size_t Count>
bool readName(std::istream& in, const char* const (&names)[Count], Enum& value) {
std::string name;
in >> name;
for (size_t index = 0; index < Count; index++) {
if (name == names[index]) {
value = (Enum) index;
return true;
}
}
return false;
}
void writeVector(std::ostream& out, Vec2F value) { out << ' ' << value.x << ' ' << value.y; }
void readVector(std::istream& in, Vec2F& value) { in >> value.x >> value.y; }
std::string escapedText(const std::string& text) {
std::string escaped;
for (const unsigned char byte : text) {
if (byte <= ' ' || byte == '%' || byte == 0x7f) {
char code[4];
std::snprintf(code, sizeof(code), "%%%02X", byte);
escaped += code;
} else {
escaped += (char) byte;
}
}
return escaped;
}
std::optional<std::string> unescapedText(const std::string& escaped) {
std::string text;
for (size_t index = 0; index < escaped.size(); index++) {
if (escaped[index] != '%') {
text += escaped[index];
continue;
}
if (index + 2 >= escaped.size()) {
return std::nullopt;
}
text += (char) std::stoi(escaped.substr(index + 1, 2), nullptr, 16);
index += 2;
}
return text;
}
// Recording format of each InputEvent alternative. A missing specialization fails to compile.
template <typename Event>
struct RecordedFormat;
template <>
struct RecordedFormat<PointerSampled> {
static constexpr const char* kName = "sample";
static void write(std::ostream& out, const PointerSampled& event) {
const PointerSample& sample = event.sample;
writeName(out, kDeviceNames, sample.device);
writeName(out, kTipNames, sample.tip);
out << ' ' << sample.pressure;
writeVector(out, sample.tilt);
out << ' ' << sample.rotation << ' ' << sample.distance << ' ' << sample.timeMs;
}
static bool read(std::istream& in, PointerSampled& event) {
PointerSample& sample = event.sample;
if (!readName(in, kDeviceNames, sample.device) || !readName(in, kTipNames, sample.tip)) {
return false;
}
in >> sample.pressure;
readVector(in, sample.tilt);
in >> sample.rotation >> sample.distance >> sample.timeMs;
return true;
}
};
template <>
struct RecordedFormat<PointerMoved> {
static constexpr const char* kName = "move";
static void write(std::ostream& out, const PointerMoved& event) { writeVector(out, event.position); }
static bool read(std::istream& in, PointerMoved& event) {
readVector(in, event.position);
return true;
}
};
template <>
struct RecordedFormat<PointerLeft> {
static constexpr const char* kName = "leave";
static void write(std::ostream&, const PointerLeft&) {}
static bool read(std::istream&, PointerLeft&) { return true; }
};
template <>
struct RecordedFormat<PointerButtonChanged> {
static constexpr const char* kName = "button";
static void write(std::ostream& out, const PointerButtonChanged& event) {
writeName(out, kButtonNames, event.button);
out << ' ' << (int) event.pressed;
}
static bool read(std::istream& in, PointerButtonChanged& event) {
int pressed = 0;
if (!readName(in, kButtonNames, event.button)) {
return false;
}
in >> pressed;
event.pressed = pressed != 0;
return true;
}
};
template <>
struct RecordedFormat<Scrolled> {
static constexpr const char* kName = "scroll";
static void write(std::ostream& out, const Scrolled& event) {
writeName(out, kScrollSourceNames, event.source);
writeVector(out, event.delta);
}
static bool read(std::istream& in, Scrolled& event) {
if (!readName(in, kScrollSourceNames, event.source)) {
return false;
}
readVector(in, event.delta);
return true;
}
};
template <>
struct RecordedFormat<ScrollEnded> {
static constexpr const char* kName = "scroll-end";
static void write(std::ostream&, const ScrollEnded&) {}
static bool read(std::istream&, ScrollEnded&) { return true; }
};
template <>
struct RecordedFormat<KeyChanged> {
static constexpr const char* kName = "key";
static void write(std::ostream& out, const KeyChanged& event) {
out << ' ' << (unsigned) event.key;
writeName(out, kKeyActionNames, event.action);
}
static bool read(std::istream& in, KeyChanged& event) {
unsigned code = 0;
in >> code;
if (code >= kKeyCodeLimit) {
return false;
}
event.key = (Key) code;
return readName(in, kKeyActionNames, event.action);
}
};
template <>
struct RecordedFormat<TextEntered> {
static constexpr const char* kName = "text";
static void write(std::ostream& out, const TextEntered& event) { out << ' ' << escapedText(event.utf8); }
static bool read(std::istream& in, TextEntered& event) {
std::string escaped;
in >> escaped;
const std::optional<std::string> text = unescapedText(escaped);
if (!text) {
return false;
}
event.utf8 = *text;
return true;
}
};
template <>
struct RecordedFormat<KeyboardFocusChanged> {
static constexpr const char* kName = "focus";
static void write(std::ostream& out, const KeyboardFocusChanged& event) { out << ' ' << (int) event.focused; }
static bool read(std::istream& in, KeyboardFocusChanged& event) {
int focused = 0;
in >> focused;
event.focused = focused != 0;
return true;
}
};
template <>
struct RecordedFormat<GestureChanged> {
static constexpr const char* kName = "gesture";
static void write(std::ostream& out, const GestureChanged& event) {
const TouchGesture& gesture = event.gesture;
writeName(out, kGesturePhaseNames, gesture.phase);
writeVector(out, gesture.center);
writeVector(out, gesture.pan);
out << ' ' << gesture.scale << ' ' << gesture.rotation << ' ' << gesture.tapFingers;
}
static bool read(std::istream& in, GestureChanged& event) {
TouchGesture& gesture = event.gesture;
if (!readName(in, kGesturePhaseNames, gesture.phase)) {
return false;
}
readVector(in, gesture.center);
readVector(in, gesture.pan);
in >> gesture.scale >> gesture.rotation >> gesture.tapFingers;
return true;
}
};
template <size_t... Indices>
std::optional<InputEvent> readEventOf(const std::string& name, std::istream& in, std::index_sequence<Indices...>) {
std::optional<InputEvent> result;
const auto tryAlternative = [&]<size_t Index>() {
using Event = std::variant_alternative_t<Index, InputEvent>;
if (result || name != RecordedFormat<Event>::kName) {
return;
}
Event event;
if (RecordedFormat<Event>::read(in, event)) {
result = event;
}
};
(tryAlternative.template operator()<Indices>(), ...);
return result;
}
std::optional<InputEvent> readEvent(const std::string& name, std::istream& in) {
return readEventOf(name, in, std::make_index_sequence<std::variant_size_v<InputEvent>>{});
}
void writeEvent(std::ostream& out, const InputEvent& event) {
std::visit(
[&out]<typename Event>(const Event& alternative) {
out << RecordedFormat<Event>::kName;
RecordedFormat<Event>::write(out, alternative);
},
event
);
}
bool isPointerActivity(const InputEvent& event) {
return std::holds_alternative<PointerSampled>(event) || std::holds_alternative<PointerMoved>(event) ||
std::holds_alternative<PointerButtonChanged>(event) || std::holds_alternative<PointerLeft>(event);
}
double steadyMs() {
const auto now = std::chrono::steady_clock::now().time_since_epoch();
return std::chrono::duration<double, std::milli>(now).count();
}
}
std::string InputRecording::serialize() const {
std::ostringstream text;
text << std::setprecision(9);
text << kHeader << "\n";
text << "window " << windowSize.x << ' ' << windowSize.y << "\n";
for (const RecordedInput& recorded : events) {
text << std::fixed << std::setprecision(3) << recorded.timeMs << ' ' << std::defaultfloat << std::setprecision(9);
writeEvent(text, recorded.event);
text << "\n";
}
return text.str();
}
std::optional<InputRecording> InputRecording::parse(const std::string& text) {
std::istringstream input(text);
std::string line;
if (!std::getline(input, line) || line != kHeader) {
return std::nullopt;
}
InputRecording recording;
while (std::getline(input, line)) {
if (line.empty()) {
continue;
}
std::istringstream fields(line);
std::string first;
fields >> first;
if (first == "window") {
readVector(fields, recording.windowSize);
continue;
}
std::string name;
fields >> name;
const std::optional<InputEvent> event = readEvent(name, fields);
if (!event || fields.fail()) {
return std::nullopt;
}
recording.events.push_back({ std::stod(first), *event });
}
return recording;
}
bool InputRecording::save(const std::string& path) const {
std::ofstream file(path);
if (!file) {
return false;
}
file << serialize();
return (bool) file;
}
std::optional<InputRecording> InputRecording::load(const std::string& path) {
std::ifstream file(path);
if (!file) {
return std::nullopt;
}
std::stringstream text;
text << file.rdbuf();
return parse(text.str());
}
void InputRecording::replayInto(InputSink& sink) const {
for (const RecordedInput& recorded : events) {
sink.post(recorded.event);
}
}
InputRecording InputRecording::translated(Vec2F offset) const {
InputRecording moved = *this;
for (RecordedInput& recorded : moved.events) {
if (auto* pointer = std::get_if<PointerMoved>(&recorded.event)) {
pointer->position += offset;
}
if (auto* gesture = std::get_if<GestureChanged>(&recorded.event)) {
gesture->gesture.center += offset;
}
}
return moved;
}
InputRecording InputRecording::slicedToPointerActivity() const {
InputRecording sliced;
sliced.windowSize = windowSize;
size_t first = events.size();
size_t last = 0;
for (size_t index = 0; index < events.size(); index++) {
if (!std::holds_alternative<PointerButtonChanged>(events[index].event)) {
continue;
}
first = std::min(first, index);
last = index;
}
if (first >= events.size()) {
return sliced;
}
while (first > 0 && isPointerActivity(events[first - 1].event) &&
!std::holds_alternative<PointerButtonChanged>(events[first - 1].event)) {
first--;
}
const double origin = events[first].timeMs;
for (size_t index = first; index <= last; index++) {
if (!isPointerActivity(events[index].event)) {
continue;
}
sliced.events.push_back({ events[index].timeMs - origin, events[index].event });
}
return sliced;
}
InputRecorder::InputRecorder(Clock clock) :
mClock(clock ? std::move(clock) : Clock(steadyMs)) {}
void InputRecorder::post(const InputEvent& event) {
const double now = mClock();
if (mStartMs < 0) {
mStartMs = now;
}
mRecording.events.push_back({ now - mStartMs, event });
}
}

View file

@ -0,0 +1,134 @@
#include "InputState.hpp"
using namespace tp;
namespace {
void press(ButtonState& state) {
if (!state.down) {
state.pressed = true;
}
state.down = true;
}
void release(ButtonState& state) {
if (state.down) {
state.released = true;
}
state.down = false;
}
}
halnf InputState::pointerPressure() const {
if (mPointerSample.isStylus()) {
return mPointerSample.pressure;
}
return button(PointerButton::Left).down ? 1.0f : 0.0f;
}
void InputState::beginFrame(std::vector<InputEvent> events) {
mPreviousPointer = mPointer;
mScrollDelta = { 0, 0 };
mContinuousScroll = false;
mScrollEnded = false;
for (ButtonState& state : mPointerButtons) {
state.pressed = false;
state.released = false;
}
for (ButtonState& state : mKeys) {
state.pressed = false;
state.released = false;
}
mFrameEvents = std::move(events);
for (const InputEvent& event : mFrameEvents) {
apply(event);
}
}
void InputState::apply(const InputEvent& event) {
std::visit(
InputEventVisitor{
[this](const PointerSampled& sampled) { mPointerSample = sampled.sample; },
[this](const PointerMoved& moved) {
mPointer = moved.position;
mPointerInside = true;
},
[this](const PointerLeft&) { mPointerInside = false; },
[this](const PointerButtonChanged& changed) {
ButtonState& state = mPointerButtons[(size_t) changed.button];
changed.pressed ? press(state) : release(state);
},
[this](const Scrolled& scrolled) {
mScrollDelta += scrolled.delta;
mContinuousScroll = scrolled.source == ScrollSource::Touchpad;
},
[this](const ScrollEnded&) { mScrollEnded = true; },
[this](const KeyChanged& changed) {
ButtonState& state = mKeys[(size_t) changed.key];
switch (changed.action) {
case KeyAction::Press: press(state); break;
case KeyAction::Release: release(state); break;
case KeyAction::Repeat: break;
}
},
[](const TextEntered&) {},
[](const KeyboardFocusChanged&) {},
[](const GestureChanged&) {},
},
event
);
}
void InputQueue::post(const InputEvent& event) {
if (std::holds_alternative<PointerLeft>(event)) {
releaseHeldPointerButtons();
}
if (const auto* focus = std::get_if<KeyboardFocusChanged>(&event); focus && !focus->focused) {
releaseHeldKeys();
}
if (!tracksHeldInput(event)) {
return;
}
mPendingEvents.push_back(event);
}
bool InputQueue::tracksHeldInput(const InputEvent& event) {
if (const auto* changed = std::get_if<PointerButtonChanged>(&event)) {
if (changed->pressed) {
mHeldPointerButtons.insert(changed->button);
return true;
}
return mHeldPointerButtons.erase(changed->button) > 0;
}
if (const auto* changed = std::get_if<KeyChanged>(&event)) {
if (changed->action == KeyAction::Press) {
mHeldKeys.insert(changed->key);
} else if (changed->action == KeyAction::Release) {
mHeldKeys.erase(changed->key);
}
}
return true;
}
void InputQueue::releaseHeldPointerButtons() {
const std::set<PointerButton> held = std::move(mHeldPointerButtons);
mHeldPointerButtons.clear();
for (PointerButton button : held) {
mPendingEvents.push_back(PointerButtonChanged{ button, false });
}
}
void InputQueue::releaseHeldKeys() {
const std::set<Key> held = std::move(mHeldKeys);
mHeldKeys.clear();
for (Key key : held) {
mPendingEvents.push_back(KeyChanged{ key, KeyAction::Release });
}
}
void InputQueue::applyPendingEvents() {
std::vector<InputEvent> events;
events.swap(mPendingEvents);
mState.beginFrame(std::move(events));
}

View file

@ -0,0 +1,122 @@
#include "TouchGesture.hpp"
#include <algorithm>
#include <cmath>
#include <numbers>
namespace tp {
namespace {
constexpr halnf kMinimumSpan = 1.0f;
}
void TouchGestureTracker::track(const TouchPoint& first, const TouchPoint& second) {
const Vec2F offset = second.position - first.position;
mFirst = first.id;
mSecond = second.id;
mCenter = (first.position + second.position) * 0.5f;
mDistance = std::sqrt(offset.x * offset.x + offset.y * offset.y);
mAngle = std::atan2(offset.y, offset.x);
}
void TouchGestureTracker::followTap(const std::vector<TouchPoint>& touches, uint32_t timeMs) {
if (mTapStarts.empty() && !touches.empty()) {
mTapStartMs = timeMs;
mTapStillPossible = true;
}
for (const TouchPoint& touch : touches) {
auto start = std::find_if(mTapStarts.begin(), mTapStarts.end(), [&](const TouchPoint& known) {
return known.id == touch.id;
});
if (start == mTapStarts.end()) {
mTapStarts.push_back(touch);
continue;
}
const Vec2F travel = touch.position - start->position;
if (travel.x * travel.x + travel.y * travel.y > kTapMaximumTravel * kTapMaximumTravel) {
mTapStillPossible = false;
}
}
}
int32_t TouchGestureTracker::finishedTapFingers(uint32_t timeMs) const {
const bool quick = timeMs - mTapStartMs <= kTapMaximumMs;
return mTapStillPossible && quick && mTapStarts.size() >= 2 ? (int32_t) mTapStarts.size() : 0;
}
void TouchGestureTracker::update(
const std::vector<TouchPoint>& touches, std::vector<TouchGesture>& gestures, uint32_t timeMs
) {
followTap(touches, timeMs);
if (touches.empty()) {
const int32_t tapFingers = finishedTapFingers(timeMs);
mTapStarts.clear();
cancel(gestures);
if (!gestures.empty() && gestures.back().phase == GesturePhase::End) {
gestures.back().tapFingers = tapFingers;
}
return;
}
if (touches.size() < 2) {
if (finishedTapFingers(timeMs) == 0) {
cancel(gestures);
}
return;
}
const TouchPoint& first = touches[0];
const TouchPoint& second = touches[1];
if (!mActive) {
track(first, second);
mActive = true;
gestures.push_back({ GesturePhase::Begin, mCenter });
return;
}
if (first.id != mFirst || second.id != mSecond) {
track(first, second);
return;
}
const Vec2F previousCenter = mCenter;
const halnf previousDistance = mDistance;
const halnf previousAngle = mAngle;
track(first, second);
TouchGesture gesture;
gesture.phase = GesturePhase::Update;
gesture.center = mCenter;
gesture.pan = mCenter - previousCenter;
gesture.scale = previousDistance >= kMinimumSpan && mDistance >= kMinimumSpan ? mDistance / previousDistance : 1;
gesture.rotation = wrapAngle(mAngle - previousAngle);
gestures.push_back(gesture);
}
void TouchGestureTracker::cancel(std::vector<TouchGesture>& gestures) {
if (!mActive) {
return;
}
mActive = false;
gestures.push_back({ GesturePhase::End, mCenter });
}
halnf wrapAngle(halnf radians) {
constexpr halnf pi = std::numbers::pi_v<halnf>;
while (radians > pi) {
radians -= 2 * pi;
}
while (radians < -pi) {
radians += 2 * pi;
}
return radians;
}
void accumulate(TouchGesture& into, const TouchGesture& next) {
into.center = next.center;
into.pan += next.pan;
into.scale *= next.scale;
into.rotation = wrapAngle(into.rotation + next.rotation);
}
}

View file

@ -0,0 +1,52 @@
#pragma once
#include "InputSink.hpp"
#include <cstdint>
#include <vector>
namespace tp {
enum class DeliveryPolicy { Every, Latest, Accumulate };
struct InputDeliveryPolicies {
DeliveryPolicy pointerMotion = DeliveryPolicy::Latest;
DeliveryPolicy scroll = DeliveryPolicy::Accumulate;
};
struct CoalescingStatistics {
uint64_t received = 0;
uint64_t delivered = 0;
};
// Backends post raw events here; they are batched and delivered once per frame.
class InputCoalescer final : public InputSink {
public:
void post(const InputEvent& event) override;
void setTap(InputSink* tap) { mTap = tap; }
void setPolicies(const InputDeliveryPolicies& policies) { mPolicies = policies; }
[[nodiscard]] bool isEmpty() const { return mEvents.empty(); }
void deliver(InputSink& sink);
[[nodiscard]] const CoalescingStatistics& statistics() const { return mStatistics; }
private:
struct Pending {
InputEvent event;
PointerSample sample;
};
[[nodiscard]] bool merge(const InputEvent& event);
[[nodiscard]] static bool carriesSample(const InputEvent& event);
[[nodiscard]] static bool isEmptyScroll(const InputEvent& event);
InputSink* mTap = nullptr;
InputDeliveryPolicies mPolicies;
std::vector<Pending> mEvents;
PointerSample mSample;
PointerSample mDeliveredSample;
CoalescingStatistics mStatistics;
};
}

View file

@ -0,0 +1,137 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace tp {
enum class PointerButton : uint8_t { Left, Right, Middle, Back, Forward };
inline constexpr size_t kPointerButtonCount = 5;
enum class Key : uint16_t {
Space = 32,
Apostrophe = 39,
Comma = 44,
Minus = 45,
Period = 46,
Slash = 47,
Digit0 = 48,
Digit1 = 49,
Digit2 = 50,
Digit3 = 51,
Digit4 = 52,
Digit5 = 53,
Digit6 = 54,
Digit7 = 55,
Digit8 = 56,
Digit9 = 57,
Semicolon = 59,
Equal = 61,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
LeftBracket = 91,
Backslash = 92,
RightBracket = 93,
GraveAccent = 96,
World1 = 161,
World2 = 162,
Escape = 256,
Enter = 257,
Tab = 258,
Backspace = 259,
Insert = 260,
Delete = 261,
Right = 262,
Left = 263,
Down = 264,
Up = 265,
PageUp = 266,
PageDown = 267,
Home = 268,
End = 269,
CapsLock = 280,
ScrollLock = 281,
NumLock = 282,
PrintScreen = 283,
Pause = 284,
F1 = 290,
F2 = 291,
F3 = 292,
F4 = 293,
F5 = 294,
F6 = 295,
F7 = 296,
F8 = 297,
F9 = 298,
F10 = 299,
F11 = 300,
F12 = 301,
F13 = 302,
F14 = 303,
F15 = 304,
F16 = 305,
F17 = 306,
F18 = 307,
F19 = 308,
F20 = 309,
F21 = 310,
F22 = 311,
F23 = 312,
F24 = 313,
F25 = 314,
Keypad0 = 320,
Keypad1 = 321,
Keypad2 = 322,
Keypad3 = 323,
Keypad4 = 324,
Keypad5 = 325,
Keypad6 = 326,
Keypad7 = 327,
Keypad8 = 328,
Keypad9 = 329,
KeypadDecimal = 330,
KeypadDivide = 331,
KeypadMultiply = 332,
KeypadSubtract = 333,
KeypadAdd = 334,
KeypadEnter = 335,
KeypadEqual = 336,
LeftShift = 340,
LeftControl = 341,
LeftAlt = 342,
LeftSuper = 343,
RightShift = 344,
RightControl = 345,
RightAlt = 346,
RightSuper = 347,
Menu = 348,
};
inline constexpr size_t kKeyCodeLimit = 349;
}

View file

@ -0,0 +1,93 @@
#pragma once
#include "InputCodes.hpp"
#include "PointerSample.hpp"
#include "TouchGesture.hpp"
#include "Vec.hpp"
#include <string>
#include <variant>
namespace tp {
enum class ScrollSource { Wheel, Touchpad };
enum class KeyAction { Press, Release, Repeat };
struct PointerSampled {
PointerSample sample;
[[nodiscard]] bool operator==(const PointerSampled& other) const = default;
};
struct PointerMoved {
Vec2F position = { 0, 0 };
[[nodiscard]] bool operator==(const PointerMoved& other) const = default;
};
struct PointerLeft {
[[nodiscard]] bool operator==(const PointerLeft& other) const = default;
};
struct PointerButtonChanged {
PointerButton button = PointerButton::Left;
bool pressed = false;
[[nodiscard]] bool operator==(const PointerButtonChanged& other) const = default;
};
struct Scrolled {
Vec2F delta = { 0, 0 };
ScrollSource source = ScrollSource::Wheel;
[[nodiscard]] bool operator==(const Scrolled& other) const = default;
};
struct ScrollEnded {
[[nodiscard]] bool operator==(const ScrollEnded& other) const = default;
};
struct KeyChanged {
Key key = Key::Space;
KeyAction action = KeyAction::Press;
[[nodiscard]] bool operator==(const KeyChanged& other) const = default;
};
struct TextEntered {
std::string utf8;
[[nodiscard]] bool operator==(const TextEntered& other) const = default;
};
struct KeyboardFocusChanged {
bool focused = false;
[[nodiscard]] bool operator==(const KeyboardFocusChanged& other) const = default;
};
struct GestureChanged {
TouchGesture gesture;
[[nodiscard]] bool operator==(const GestureChanged& other) const = default;
};
// The one declaration of every input event; sinks, the coalescer, input state and recordings derive from it.
using InputEvent = std::variant<
PointerSampled,
PointerMoved,
PointerLeft,
PointerButtonChanged,
Scrolled,
ScrollEnded,
KeyChanged,
TextEntered,
KeyboardFocusChanged,
GestureChanged>;
template <typename... Handlers>
struct InputEventVisitor : Handlers... {
using Handlers::operator()...;
};
}

View file

@ -0,0 +1,54 @@
#pragma once
#include "InputSink.hpp"
#include <functional>
#include <optional>
#include <string>
#include <vector>
namespace tp {
struct RecordedInput {
double timeMs = 0;
InputEvent event;
[[nodiscard]] bool operator==(const RecordedInput& other) const = default;
};
struct InputRecording {
static constexpr const char* kHeader = "tp-input-recording 2";
Vec2F windowSize = { 0, 0 };
std::vector<RecordedInput> events;
[[nodiscard]] double durationMs() const { return events.empty() ? 0.0 : events.back().timeMs; }
[[nodiscard]] std::string serialize() const;
[[nodiscard]] static std::optional<InputRecording> parse(const std::string& text);
bool save(const std::string& path) const;
[[nodiscard]] static std::optional<InputRecording> load(const std::string& path);
void replayInto(InputSink& sink) const;
[[nodiscard]] InputRecording translated(Vec2F offset) const;
[[nodiscard]] InputRecording slicedToPointerActivity() const;
};
class InputRecorder final : public InputSink {
public:
using Clock = std::function<double()>;
explicit InputRecorder(Clock clock = {});
void setWindowSize(Vec2F size) { mRecording.windowSize = size; }
void startClockNow() { mStartMs = mClock(); }
[[nodiscard]] const InputRecording& recording() const { return mRecording; }
void post(const InputEvent& event) override;
private:
Clock mClock;
double mStartMs = -1;
InputRecording mRecording;
};
}

View file

@ -0,0 +1,18 @@
#pragma once
#include "InputEvent.hpp"
namespace tp {
class InputSink {
public:
virtual ~InputSink() = default;
virtual void post(const InputEvent& event) = 0;
};
class DiscardingInputSink final : public InputSink {
public:
void post(const InputEvent&) override {}
};
}

View file

@ -0,0 +1,118 @@
#pragma once
#include "InputSink.hpp"
#include <array>
#include <cstddef>
#include <set>
#include <vector>
namespace tp {
struct ButtonState {
bool pressed = false;
bool down = false;
bool released = false;
};
// The input of one frame: the ordered event log plus the per-frame state derived from it. Read-only for consumers.
class InputState {
public:
[[nodiscard]] const std::vector<InputEvent>& events() const { return mFrameEvents; }
[[nodiscard]] Vec2F pointer() const { return mPointer; }
[[nodiscard]] Vec2F previousPointer() const { return mPreviousPointer; }
[[nodiscard]] Vec2F pointerDelta() const { return mPointer - mPreviousPointer; }
[[nodiscard]] bool isPointerInside() const { return mPointerInside; }
[[nodiscard]] const PointerSample& pointerSample() const { return mPointerSample; }
[[nodiscard]] halnf pointerPressure() const;
[[nodiscard]] const ButtonState& button(PointerButton button) const { return mPointerButtons[(size_t) button]; }
[[nodiscard]] const ButtonState& key(Key key) const { return mKeys[(size_t) key]; }
[[nodiscard]] Vec2F scroll() const { return mScrollDelta; }
[[nodiscard]] bool isContinuousScroll() const { return mContinuousScroll; }
[[nodiscard]] bool isScrollEnded() const { return mScrollEnded; }
private:
friend class InputQueue;
void beginFrame(std::vector<InputEvent> events);
void apply(const InputEvent& event);
std::vector<InputEvent> mFrameEvents;
Vec2F mPointer = { 0, 0 };
Vec2F mPreviousPointer = { 0, 0 };
bool mPointerInside = false;
PointerSample mPointerSample;
Vec2F mScrollDelta = { 0, 0 };
bool mContinuousScroll = false;
bool mScrollEnded = false;
std::array<ButtonState, kPointerButtonCount> mPointerButtons{};
std::array<ButtonState, kKeyCodeLimit> mKeys{};
};
// Collects delivered events between frames and releases held buttons and keys when focus or the pointer is lost.
class InputQueue final : public InputSink {
public:
void post(const InputEvent& event) override;
[[nodiscard]] size_t pendingEventCount() const { return mPendingEvents.size(); }
void applyPendingEvents();
[[nodiscard]] const InputState& state() const { return mState; }
private:
void releaseHeldPointerButtons();
void releaseHeldKeys();
[[nodiscard]] bool tracksHeldInput(const InputEvent& event);
std::vector<InputEvent> mPendingEvents;
std::set<PointerButton> mHeldPointerButtons;
std::set<Key> mHeldKeys;
InputState mState;
};
// A consumer's view of one frame: pointer coordinates relative to an origin, optionally without button/key state.
class InputView {
public:
enum class Buttons { Visible, Hidden };
explicit InputView(const InputState& state, Vec2F origin = { 0, 0 }, Buttons buttons = Buttons::Visible) :
mState(state),
mOrigin(origin),
mButtons(buttons) {}
[[nodiscard]] InputView relativeTo(Vec2F origin) const { return InputView(mState, origin, mButtons); }
[[nodiscard]] InputView withButtons(Buttons buttons) const { return InputView(mState, mOrigin, buttons); }
[[nodiscard]] const InputState& state() const { return mState; }
[[nodiscard]] Vec2F getPointer() const { return mState.pointer() - mOrigin; }
[[nodiscard]] Vec2F getPointerPrev() const { return mState.previousPointer() - mOrigin; }
[[nodiscard]] Vec2F getPointerDelta() const { return mState.pointerDelta(); }
[[nodiscard]] halnf getPointerPressure() const { return areButtonsVisible() ? mState.pointerPressure() : 0; }
[[nodiscard]] bool isPressed(PointerButton button) const { return visible(mState.button(button)).pressed; }
[[nodiscard]] bool isReleased(PointerButton button) const { return visible(mState.button(button)).released; }
[[nodiscard]] bool isDown(PointerButton button) const { return visible(mState.button(button)).down; }
[[nodiscard]] bool isPressed(Key key) const { return visible(mState.key(key)).pressed; }
[[nodiscard]] bool isReleased(Key key) const { return visible(mState.key(key)).released; }
[[nodiscard]] bool isDown(Key key) const { return visible(mState.key(key)).down; }
[[nodiscard]] halnf getScrollX() const { return mState.scroll().x; }
[[nodiscard]] halnf getScrollY() const { return mState.scroll().y; }
private:
[[nodiscard]] bool areButtonsVisible() const { return mButtons == Buttons::Visible; }
[[nodiscard]] ButtonState visible(const ButtonState& state) const {
return areButtonsVisible() ? state : ButtonState{};
}
const InputState& mState;
Vec2F mOrigin;
Buttons mButtons;
};
}

Some files were not shown because too many files have changed in this diff Show more