From 738b6abda8994e2a83ca6d246b2651c5e3dfe709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 10:09:06 +0000 Subject: [PATCH 01/17] fix: critical Vulkan/Wayland bugs, resource leaks, and build improvements - Fix descriptor set layout: store as class member, wire into allocation - Fix render pass format mismatch: use dynamic swapchain format - Fix Wayland event loop: proper prepare_read/cancel_read/read_events - Add queue family enumeration: find graphics+present capable family - Fix eventfd busy-loop: consume wake events after poll - Add SPIR-V shader compilation (glslc) to CMakeLists.txt - Fix full Vulkan resource cleanup in destructor (atlas, vertex buf, descriptors, Wayland objects) - Remove all debug std::cout logging, remove unused iostream includes - Fix CMake -Oz flag conflict with -O3 in release - Use $SHELL env with fish fallback instead of hardcoded /usr/bin/fish - Adopt Veridian Zenith 'Atmosphere' color palette (#050200 bg, #f3f4f6 fg, #FFB347 accent) - Add glslang-tools to CI dependencies - Add shaders/*.spv to .gitignore --- .github/workflows/codeql.yml | 2 +- .gitignore | 3 + CMakeLists.txt | 24 ++++- include/common.hpp | 10 +- include/sigil.hpp | 4 + src/glyph_engine.cpp | 1 - src/loom.cpp | 11 +- src/main.cpp | 17 ++- src/nexus.cpp | 3 +- src/sigil.cpp | 198 +++++++++++++++++------------------ 10 files changed, 146 insertions(+), 127 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c747f1e..7ef9be5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -36,7 +36,7 @@ jobs: - name: Install dependencies and oneAPI run: | sudo apt-get update - sudo apt-get install -y liburing-dev libvulkan-dev libwayland-dev libfreetype6-dev libfontconfig1-dev wget ninja-build ccache mold + sudo apt-get install -y liburing-dev libvulkan-dev libwayland-dev libfreetype6-dev libfontconfig1-dev wget ninja-build ccache mold glslang-tools # Install Intel oneAPI Base Toolkit (Compiler) wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB | gpg --dearmor | sudo tee /usr/share/keyrings/oneapi-archive-keyring.gpg > /dev/null diff --git a/.gitignore b/.gitignore index e210ea6..6977dee 100644 --- a/.gitignore +++ b/.gitignore @@ -20,5 +20,8 @@ Thumbs.db *.dll *.exe +# Compiled shaders (generated from GLSL sources) +shaders/*.spv + # Logs *.log diff --git a/CMakeLists.txt b/CMakeLists.txt index 2529548..3e70b16 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,7 +45,7 @@ endif() # ---- Compiler & Feature Flags ---- # Optimized for performance and security, mirroring Voix's professional standards -set(COMMON_FLAGS "-march=${KAELUM_ARCH} -Oz -pipe -fno-plt -fstack-protector-strong \ +set(COMMON_FLAGS "-march=${KAELUM_ARCH} -pipe -fno-plt -fstack-protector-strong \ -fstack-clash-protection -D_FORTIFY_SOURCE=3 -fPIE -flto=thin -fvisibility=hidden \ -fexperimental-library -std=c++26") @@ -150,6 +150,28 @@ target_link_libraries(kaelum PRIVATE kaelum_lib) target_compile_options(kaelum_lib PRIVATE -Wall -Wextra) target_compile_options(kaelum PRIVATE -Wall -Wextra) +# ---- Shader Compilation ---- +find_program(GLSLC glslc) +if(GLSLC) + set(SHADER_DIR ${CMAKE_SOURCE_DIR}/shaders) + set(SHADER_SOURCES ${SHADER_DIR}/test.vert ${SHADER_DIR}/test.frag) + foreach(SHADER_SRC ${SHADER_SOURCES}) + get_filename_component(SHADER_NAME ${SHADER_SRC} NAME) + set(SHADER_SPV ${SHADER_DIR}/${SHADER_NAME}.spv) + add_custom_command( + OUTPUT ${SHADER_SPV} + COMMAND ${GLSLC} ${SHADER_SRC} -o ${SHADER_SPV} + DEPENDS ${SHADER_SRC} + COMMENT "Compiling shader ${SHADER_NAME}" + ) + list(APPEND SHADER_SPV_FILES ${SHADER_SPV}) + endforeach() + add_custom_target(shaders ALL DEPENDS ${SHADER_SPV_FILES}) + add_dependencies(kaelum shaders) +else() + message(WARNING "glslc not found. Shaders will not be compiled automatically.") +endif() + # ---- Installation ---- include(GNUInstallDirs) install(TARGETS kaelum RUNTIME DESTINATION bin) diff --git a/include/common.hpp b/include/common.hpp index 88e9522..2e4baff 100644 --- a/include/common.hpp +++ b/include/common.hpp @@ -21,16 +21,18 @@ namespace Kaelum { constexpr bool operator==(const Color&) const = default; }; - constexpr Color nord_bg = {46, 52, 64, 255}; - constexpr Color nord_fg = {216, 222, 233, 255}; + // Veridian Zenith "Atmosphere" palette — deep void with amber accents + constexpr Color vz_bg = {5, 2, 0, 255}; // --vz-bg-primary #050200 + constexpr Color vz_fg = {243, 244, 246, 255}; // body text #f3f4f6 + constexpr Color vz_accent = {255, 179, 71, 255}; // --vz-accent-vibrant #FFB347 /** * @brief A single terminal cell */ struct Cell { char32_t codepoint = U' '; - Color fg = nord_fg; - Color bg = nord_bg; + Color fg = vz_fg; + Color bg = vz_bg; uint32_t attrs = 0; // Bold, Italic, Underline, etc. constexpr bool operator==(const Cell&) const = default; diff --git a/include/sigil.hpp b/include/sigil.hpp index e7fd800..ae57d81 100644 --- a/include/sigil.hpp +++ b/include/sigil.hpp @@ -75,6 +75,7 @@ namespace Kaelum { void poll_events(); void prepare_read() { wl_display_prepare_read(display_); } + void cancel_read() { wl_display_cancel_read(display_); } void dispatch_pending() { wl_display_dispatch_pending(display_); } /** @@ -116,6 +117,9 @@ namespace Kaelum { VkRenderPass render_pass_ = VK_NULL_HANDLE; VkPipeline graphics_pipeline_ = VK_NULL_HANDLE; VkPipelineLayout pipeline_layout_ = VK_NULL_HANDLE; + VkDescriptorSetLayout descriptor_set_layout_ = VK_NULL_HANDLE; + VkFormat swapchain_format_ = VK_FORMAT_B8G8R8A8_SRGB; + uint32_t graphics_queue_family_ = 0; // Command buffers VkCommandPool command_pool_ = VK_NULL_HANDLE; diff --git a/src/glyph_engine.cpp b/src/glyph_engine.cpp index 65ba2ed..ddaabd5 100644 --- a/src/glyph_engine.cpp +++ b/src/glyph_engine.cpp @@ -1,5 +1,4 @@ #include "glyph_engine.hpp" -#include #include #include diff --git a/src/loom.cpp b/src/loom.cpp index f9eb879..99477c4 100644 --- a/src/loom.cpp +++ b/src/loom.cpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include #include #include #include @@ -34,9 +34,12 @@ std::expected Loom::initialize() { } if (child_pid_ == 0) { - // Child: Execute Fish Shell - execl("/usr/bin/fish", "fish", nullptr); - // If execl fails + // Child: Execute the user's preferred shell, with Fish as first preference + const char* shell = std::getenv("SHELL"); + if (!shell || shell[0] == '\0') shell = "/usr/bin/fish"; + execl(shell, shell, nullptr); + // If execl fails, try /bin/sh as last resort + execl("/bin/sh", "sh", nullptr); std::exit(1); } diff --git a/src/main.cpp b/src/main.cpp index 4c960a5..2290979 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,10 +1,8 @@ -#include #include #include -#include -#include #include #include +#include #include "loom.hpp" #include "nexus.hpp" #include "sigil.hpp" @@ -27,16 +25,13 @@ int main() { std::println(stderr, "Failed to initialize Loom."); return 1; } - std::cout << "Main: Loom initialized" << std::endl; auto sigil_res = sigil.initialize(); if (!sigil_res) { std::println(stderr, "Fatal: Sigil initialization failed."); return 1; } - std::cout << "Main: Sigil initialized" << std::endl; - - std::cout << "Main: Initializing assets..." << std::endl; + sigil.initialize_assets(glyphs); @@ -88,15 +83,17 @@ int main() { } if (poll_fds[0].revents & POLLIN) { - // Read events from Wayland and dispatch them - // We use poll_events() which is basically dispatch sigil.poll_events(); } else { - // Still dispatch pending events even if we didn't read new ones + sigil.cancel_read(); sigil.dispatch_pending(); } if (poll_fds[1].revents & POLLIN) { + // Consume the eventfd to prevent busy-loop + uint64_t wake_val; + [[maybe_unused]] auto r = ::read(wake_fd, &wake_val, sizeof(wake_val)); + auto read_res = loom.poll_read(buffer); if (read_res) { size_t bytes = *read_res; diff --git a/src/nexus.cpp b/src/nexus.cpp index 2cb70a3..66156f0 100644 --- a/src/nexus.cpp +++ b/src/nexus.cpp @@ -1,5 +1,4 @@ #include "nexus.hpp" -#include #include #include @@ -26,7 +25,7 @@ void Nexus::handle_ground(uint8_t c) { } else if (c == '\t') { cursor_x_ = (cursor_x_ + 8) % k_default_cols; } else { - set_cell(static_cast(c), {255, 255, 255, 255}, {0, 0, 0, 255}, 0); + set_cell(static_cast(c), vz_fg, vz_bg, 0); cursor_x_ = (cursor_x_ + 1) % k_default_cols; if (cursor_x_ == 0) { cursor_y_ = (cursor_y_ + 1) % k_default_rows; diff --git a/src/sigil.cpp b/src/sigil.cpp index eac63d6..a0c07cf 100644 --- a/src/sigil.cpp +++ b/src/sigil.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -21,13 +20,31 @@ namespace Kaelum { Sigil::Sigil() = default; Sigil::~Sigil() { + if (device_ != VK_NULL_HANDLE) vkDeviceWaitIdle(device_); + if (swapchain_ != VK_NULL_HANDLE) { cleanup_swapchain(); vkDestroySwapchainKHR(device_, swapchain_, nullptr); } + + // Glyph atlas resources + if (glyph_atlas_sampler_ != VK_NULL_HANDLE) vkDestroySampler(device_, glyph_atlas_sampler_, nullptr); + if (glyph_atlas_view_ != VK_NULL_HANDLE) vkDestroyImageView(device_, glyph_atlas_view_, nullptr); + if (glyph_atlas_image_ != VK_NULL_HANDLE) vkDestroyImage(device_, glyph_atlas_image_, nullptr); + if (glyph_atlas_memory_ != VK_NULL_HANDLE) vkFreeMemory(device_, glyph_atlas_memory_, nullptr); + + // Vertex buffer resources + if (vertex_buffer_mapped_) vkUnmapMemory(device_, vertex_buffer_memory_); + if (vertex_buffer_ != VK_NULL_HANDLE) vkDestroyBuffer(device_, vertex_buffer_, nullptr); + if (vertex_buffer_memory_ != VK_NULL_HANDLE) vkFreeMemory(device_, vertex_buffer_memory_, nullptr); + + // Descriptor resources + if (descriptor_pool_ != VK_NULL_HANDLE) vkDestroyDescriptorPool(device_, descriptor_pool_, nullptr); + if (descriptor_set_layout_ != VK_NULL_HANDLE) vkDestroyDescriptorSetLayout(device_, descriptor_set_layout_, nullptr); + if (command_pool_ != VK_NULL_HANDLE) vkDestroyCommandPool(device_, command_pool_, nullptr); - if (pipeline_layout_ != VK_NULL_HANDLE) vkDestroyPipelineLayout(device_, pipeline_layout_, nullptr); if (graphics_pipeline_ != VK_NULL_HANDLE) vkDestroyPipeline(device_, graphics_pipeline_, nullptr); + if (pipeline_layout_ != VK_NULL_HANDLE) vkDestroyPipelineLayout(device_, pipeline_layout_, nullptr); if (render_pass_ != VK_NULL_HANDLE) vkDestroyRenderPass(device_, render_pass_, nullptr); if (image_available_semaphore_ != VK_NULL_HANDLE) vkDestroySemaphore(device_, image_available_semaphore_, nullptr); if (render_finished_semaphore_ != VK_NULL_HANDLE) vkDestroySemaphore(device_, render_finished_semaphore_, nullptr); @@ -35,9 +52,16 @@ Sigil::~Sigil() { if (device_ != VK_NULL_HANDLE) vkDestroyDevice(device_, nullptr); if (vk_surface_ != VK_NULL_HANDLE) vkDestroySurfaceKHR(instance_, vk_surface_, nullptr); if (instance_ != VK_NULL_HANDLE) vkDestroyInstance(instance_, nullptr); - + + // Wayland cleanup + if (keyboard_) wl_keyboard_destroy(keyboard_); + if (xdg_toplevel_) xdg_toplevel_destroy(xdg_toplevel_); + if (xdg_surface_) xdg_surface_destroy(xdg_surface_); + if (xdg_wm_base_) xdg_wm_base_destroy(xdg_wm_base_); if (wl_surface_) wl_surface_destroy(wl_surface_); + if (seat_) wl_seat_destroy(seat_); if (compositor_) wl_compositor_destroy(compositor_); + if (registry_) wl_registry_destroy(registry_); if (display_) wl_display_disconnect(display_); } @@ -47,31 +71,22 @@ std::expected Sigil::initialize() { if (!w_res) return std::unexpected(w_res.error()); auto v_res = init_vulkan(); - std::cout << "Sigil: Vulkan init finished" << std::endl; if (!v_res) return std::unexpected(v_res.error()); auto s_res = create_swapchain(); - std::cout << "Sigil: Swapchain created" << std::endl; if (!s_res) return std::unexpected(s_res.error()); + create_render_pass(); - std::cout << "Sigil: Render pass created" << std::endl; create_framebuffers(); - std::cout << "Sigil: Framebuffers created" << std::endl; - create_graphics_pipeline(); - std::cout << "Sigil: Pipeline created" << std::endl; create_sync_objects(); - std::cout << "Sigil: Sync objects created" << std::endl; create_command_pool(); - std::cout << "Sigil: Command pool created" << std::endl; + create_graphics_pipeline(); record_command_buffers(); - std::cout << "Sigil: Command buffers recorded" << std::endl; create_vertex_buffer(); - std::cout << "Sigil: Vertex buffer created" << std::endl; - + auto l_res = init_level_zero(); if (!l_res) return std::unexpected(l_res.error()); - std::println("Sigil: Full hardware pipeline initialized successfully."); return {}; } @@ -143,29 +158,23 @@ static const struct wl_keyboard_listener keyboard_listener = { }; std::expected Sigil::init_wayland() { - std::cout << "Sigil: init_wayland start" << std::endl; display_ = wl_display_connect(nullptr); if (!display_) { std::println(stderr, "Sigil: Failed to connect to Wayland display."); return std::unexpected(SigilError::WaylandInitFailed); } - std::cout << "Sigil: Wayland display connected" << std::endl; registry_ = wl_display_get_registry(display_); wl_registry_add_listener(registry_, ®istry_listener, this); - std::cout << "Sigil: Roundtrip start" << std::endl; wl_display_roundtrip(display_); - std::cout << "Sigil: Roundtrip end" << std::endl; if (!compositor_ || !xdg_wm_base_) { std::println(stderr, "Sigil: Failed to bind required Wayland globals."); return std::unexpected(SigilError::WaylandInitFailed); } - std::cout << "Sigil: Globals bound" << std::endl; wl_surface_ = wl_compositor_create_surface(compositor_); if (!wl_surface_) return std::unexpected(SigilError::SurfaceCreationFailed); - std::cout << "Sigil: Surface created" << std::endl; xdg_surface_ = xdg_wm_base_get_xdg_surface(xdg_wm_base_, wl_surface_); xdg_surface_add_listener(xdg_surface_, &xdg_surface_listener, this); @@ -200,9 +209,8 @@ void Sigil::handle_key_event(uint32_t key, bool pressed) { } void Sigil::poll_events() { - while (wl_display_dispatch(display_) != 0) { - // Keep dispatching until the event queue is empty - } + wl_display_read_events(display_); + wl_display_dispatch_pending(display_); } std::expected Sigil::init_vulkan() { @@ -236,7 +244,6 @@ std::expected Sigil::init_vulkan() { vkEnumeratePhysicalDevices(instance_, &device_count, devices.data()); physical_device_ = devices[0]; - // Create Wayland Surface VkWaylandSurfaceCreateInfoKHR surface_create_info{}; surface_create_info.sType = VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR; surface_create_info.display = display_; @@ -246,19 +253,38 @@ std::expected Sigil::init_vulkan() { return std::unexpected(SigilError::VulkanInitFailed); } + // Find a queue family that supports both graphics and presentation + uint32_t queue_family_count = 0; + vkGetPhysicalDeviceQueueFamilyProperties(physical_device_, &queue_family_count, nullptr); + std::vector queue_families(queue_family_count); + vkGetPhysicalDeviceQueueFamilyProperties(physical_device_, &queue_family_count, queue_families.data()); + + graphics_queue_family_ = UINT32_MAX; + for (uint32_t i = 0; i < queue_family_count; ++i) { + VkBool32 present_support = VK_FALSE; + vkGetPhysicalDeviceSurfaceSupportKHR(physical_device_, i, vk_surface_, &present_support); + if ((queue_families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && present_support) { + graphics_queue_family_ = i; + break; + } + } + if (graphics_queue_family_ == UINT32_MAX) { + std::println(stderr, "Sigil: No queue family supports both graphics and presentation."); + return std::unexpected(SigilError::VulkanInitFailed); + } + float queue_priority = 1.0f; VkDeviceQueueCreateInfo queue_create_info{}; queue_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; - queue_create_info.queueFamilyIndex = 0; // Simplified + queue_create_info.queueFamilyIndex = graphics_queue_family_; queue_create_info.queueCount = 1; queue_create_info.pQueuePriorities = &queue_priority; - + VkDeviceCreateInfo device_create_info{}; device_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; device_create_info.queueCreateInfoCount = 1; device_create_info.pQueueCreateInfos = &queue_create_info; - // Enable swapchain extension for the device std::vector device_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; device_create_info.enabledExtensionCount = static_cast(device_extensions.size()); device_create_info.ppEnabledExtensionNames = device_extensions.data(); @@ -267,7 +293,7 @@ std::expected Sigil::init_vulkan() { return std::unexpected(SigilError::VulkanInitFailed); } - vkGetDeviceQueue(device_, 0, 0, &graphics_queue_); + vkGetDeviceQueue(device_, graphics_queue_family_, 0, &graphics_queue_); return {}; } @@ -334,6 +360,7 @@ std::expected Sigil::create_swapchain() { break; } } + swapchain_format_ = surface_format.format; // Choose present mode (prefer Mailbox for low latency, fall back to FIFO) VkPresentModeKHR present_mode = VK_PRESENT_MODE_FIFO_KHR; @@ -396,7 +423,6 @@ std::expected Sigil::create_swapchain() { std::println(stderr, "Sigil: Failed to create image view for swapchain image {}", i); } } - std::println("Sigil: Swapchain created with {} images.", image_count); return {}; } @@ -423,7 +449,7 @@ void Sigil::create_framebuffers() { void Sigil::create_render_pass() { VkAttachmentDescription color_attachment{}; - color_attachment.format = VK_FORMAT_B8G8R8A8_SRGB; + color_attachment.format = swapchain_format_; color_attachment.samples = VK_SAMPLE_COUNT_1_BIT; color_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; @@ -479,15 +505,13 @@ static VkShaderModule create_shader_module(VkDevice device, const std::string& f } void Sigil::create_graphics_pipeline() { - std::cout << "Sigil: Loading shaders" << std::endl; VkShaderModule vertShaderModule = create_shader_module(device_, "shaders/test.vert.spv"); VkShaderModule fragShaderModule = create_shader_module(device_, "shaders/test.frag.spv"); - + if (vertShaderModule == VK_NULL_HANDLE || fragShaderModule == VK_NULL_HANDLE) { std::println(stderr, "Sigil: Failed to load one or more shaders"); return; } - std::cout << "Sigil: Shaders loaded" << std::endl; VkPipelineShaderStageCreateInfo stages[2] = {}; stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; @@ -591,8 +615,7 @@ void Sigil::create_graphics_pipeline() { layout_info.bindingCount = 1; layout_info.pBindings = &layout_binding; - VkDescriptorSetLayout descriptor_set_layout = VK_NULL_HANDLE; - if (vkCreateDescriptorSetLayout(device_, &layout_info, nullptr, &descriptor_set_layout) != VK_SUCCESS) { + if (vkCreateDescriptorSetLayout(device_, &layout_info, nullptr, &descriptor_set_layout_) != VK_SUCCESS) { std::println(stderr, "Sigil: Failed to create descriptor set layout"); return; } @@ -600,11 +623,10 @@ void Sigil::create_graphics_pipeline() { VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; pipelineLayoutInfo.setLayoutCount = 1; - pipelineLayoutInfo.pSetLayouts = &descriptor_set_layout; - + pipelineLayoutInfo.pSetLayouts = &descriptor_set_layout_; + if (vkCreatePipelineLayout(device_, &pipelineLayoutInfo, nullptr, &pipeline_layout_) != VK_SUCCESS) { std::println(stderr, "Sigil: Failed to create pipeline layout"); - vkDestroyDescriptorSetLayout(device_, descriptor_set_layout, nullptr); return; } @@ -629,15 +651,12 @@ void Sigil::create_graphics_pipeline() { pipelineInfo.renderPass = render_pass_; pipelineInfo.subpass = 0; - std::cout << "Sigil: Calling vkCreateGraphicsPipelines" << std::endl; if (vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphics_pipeline_) != VK_SUCCESS) { std::println(stderr, "Sigil: Failed to create graphics pipeline"); } - std::cout << "Sigil: Graphics pipeline created" << std::endl; - + vkDestroyShaderModule(device_, vertShaderModule, nullptr); vkDestroyShaderModule(device_, fragShaderModule, nullptr); - vkDestroyDescriptorSetLayout(device_, descriptor_set_layout, nullptr); } @@ -690,17 +709,15 @@ void Sigil::render(const Nexus& nexus) { return; } - // --- Update Vertex Buffer from Nexus Grid --- const auto& grid = nexus.get_grid(); std::vector vertices; - - // Calculate aspect ratio to prevent glyph stretching + float window_aspect = (float)extent_.width / (float)extent_.height; float grid_aspect = (float)k_default_cols / (float)k_default_rows; - + float scale_x = 1.0f; float scale_y = 1.0f; - + if (window_aspect > grid_aspect) { scale_x = grid_aspect / window_aspect; } else { @@ -709,32 +726,14 @@ void Sigil::render(const Nexus& nexus) { float cell_w = (2.0f * scale_x) / k_default_cols; float cell_h = (2.0f * scale_y) / k_default_rows; - float offset_x = -1.0f * scale_x; // This is not quite right for centering - float offset_y = 1.0f; - - // Center the grid - float start_x = -1.0f + (1.0f - scale_x); - float start_y = 1.0f; - - // Wait, simpler: - // X ranges from -1 to 1. Total width 2. - // If scale_x = 0.8, we want it to go from -0.4 to 0.4. - float x_min = -scale_x; - float x_max = scale_x; - float y_min = -scale_y; - float y_max = scale_y; - - // Let's just use NDC and scale the width/height. - float current_cell_w = (2.0f * scale_x) / k_default_cols; - float current_cell_h = (2.0f * scale_y) / k_default_rows; - float current_offset_x = -scale_x; - float current_offset_y = scale_y; + float origin_x = -scale_x; + float origin_y = scale_y; for (uint32_t y = 0; y < k_default_rows; ++y) { for (uint32_t x = 0; x < k_default_cols; ++x) { const auto& cell = grid[y * k_default_cols + x]; - if (cell.codepoint == U' ') continue; // Skip empty cells for now - + if (cell.codepoint == U' ') continue; + auto it = glyph_map_.find(cell.codepoint); float u0 = 0, v0 = 0, u1 = 1, v1 = 1; if (it != glyph_map_.end()) { @@ -743,21 +742,21 @@ void Sigil::render(const Nexus& nexus) { u1 = it->second.u1; v1 = it->second.v1; } - - float x0 = current_offset_x + x * current_cell_w; - float y0 = current_offset_y - (y + 1) * current_cell_h; - float x1 = x0 + current_cell_w; - float y1 = current_offset_y - y * current_cell_h; - - // Triangle 1 - vertices.push_back({{x0, y0}, {u0, v0}, {cell.fg.r/255.0f, cell.fg.g/255.0f, cell.fg.b/255.0f, cell.fg.a/255.0f}}); - vertices.push_back({{x1, y0}, {u1, v0}, {cell.fg.r/255.0f, cell.fg.g/255.0f, cell.fg.b/255.0f, cell.fg.a/255.0f}}); - vertices.push_back({{x0, y1}, {u0, v1}, {cell.fg.r/255.0f, cell.fg.g/255.0f, cell.fg.b/255.0f, cell.fg.a/255.0f}}); - - // Triangle 2 - vertices.push_back({{x1, y0}, {u1, v0}, {cell.fg.r/255.0f, cell.fg.g/255.0f, cell.fg.b/255.0f, cell.fg.a/255.0f}}); - vertices.push_back({{x1, y1}, {u1, v1}, {cell.fg.r/255.0f, cell.fg.g/255.0f, cell.fg.b/255.0f, cell.fg.a/255.0f}}); - vertices.push_back({{x0, y1}, {u0, v1}, {cell.fg.r/255.0f, cell.fg.g/255.0f, cell.fg.b/255.0f, cell.fg.a/255.0f}}); + + float x0 = origin_x + x * cell_w; + float y0 = origin_y - (y + 1) * cell_h; + float x1 = x0 + cell_w; + float y1 = origin_y - y * cell_h; + + float r = cell.fg.r / 255.0f, g = cell.fg.g / 255.0f; + float b = cell.fg.b / 255.0f, a = cell.fg.a / 255.0f; + + vertices.push_back({{x0, y0}, {u0, v0}, {r, g, b, a}}); + vertices.push_back({{x1, y0}, {u1, v0}, {r, g, b, a}}); + vertices.push_back({{x0, y1}, {u0, v1}, {r, g, b, a}}); + vertices.push_back({{x1, y0}, {u1, v0}, {r, g, b, a}}); + vertices.push_back({{x1, y1}, {u1, v1}, {r, g, b, a}}); + vertices.push_back({{x0, y1}, {u0, v1}, {r, g, b, a}}); } } @@ -784,7 +783,8 @@ void Sigil::render(const Nexus& nexus) { beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; vkBeginCommandBuffer(cb, &beginInfo); - VkClearValue clearColor = {{{5/255.0f, 2/255.0f, 0/255.0f, 0.85f}}}; + // Veridian Zenith: deep void background (#050200) + VkClearValue clearColor = {{{5.0f/255.0f, 2.0f/255.0f, 0.0f/255.0f, 1.0f}}}; VkRenderPassBeginInfo renderPassInfo{}; renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; renderPassInfo.renderPass = render_pass_; @@ -795,7 +795,7 @@ void Sigil::render(const Nexus& nexus) { renderPassInfo.pClearValues = &clearColor; vkCmdBeginRenderPass(cb, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); - + VkViewport viewport{}; viewport.x = 0.0f; viewport.y = 0.0f; @@ -841,6 +841,8 @@ void Sigil::render(const Nexus& nexus) { } void Sigil::on_resize(uint32_t width, uint32_t height) { + (void)width; + (void)height; if (device_ == VK_NULL_HANDLE) return; vkDeviceWaitIdle(device_); cleanup_swapchain(); @@ -850,16 +852,13 @@ void Sigil::on_resize(uint32_t width, uint32_t height) { } create_framebuffers(); record_command_buffers(); - - // In a real implementation, we'd also update viewport and scissor in the pipeline - std::println("Sigil: Swapchain and framebuffers recreated for size {}x{}", width, height); } void Sigil::create_command_pool() { VkCommandPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; - poolInfo.queueFamilyIndex = 0; + poolInfo.queueFamilyIndex = graphics_queue_family_; if (vkCreateCommandPool(device_, &poolInfo, nullptr, &command_pool_) != VK_SUCCESS) { std::println(stderr, "Sigil: Failed to create command pool"); @@ -886,7 +885,7 @@ void Sigil::record_command_buffers() { beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; if (vkBeginCommandBuffer(command_buffers_[i], &beginInfo) != VK_SUCCESS) continue; - VkClearValue clearColor = {{{5/255.0f, 2/255.0f, 0/255.0f, 0.85f}}}; // Nord background + VkClearValue clearColor = {{{5.0f/255.0f, 2.0f/255.0f, 0.0f/255.0f, 1.0f}}}; VkRenderPassBeginInfo renderPassInfo{}; renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; @@ -905,7 +904,6 @@ void Sigil::record_command_buffers() { } void Sigil::create_glyph_atlas(GlyphEngine& engine) { - std::cout << "Sigil: Creating glyph atlas..." << std::endl; uint32_t atlas_width = 1024; uint32_t atlas_height = 1024; @@ -976,15 +974,12 @@ void Sigil::create_glyph_atlas(GlyphEngine& engine) { return; } - // Now pack the glyphs std::vector atlas_data(atlas_width * atlas_height, 0); uint32_t current_x = 0; uint32_t current_y = 0; uint32_t max_row_height = 0; - // For now, we'll just load ASCII 32-126 for (char32_t c = 32; c < 127; ++c) { - std::cout << "Sigil: Packing glyph " << (int)c << std::endl; auto glyph_res = engine.get_glyph(c); if (!glyph_res) continue; @@ -1017,12 +1012,9 @@ void Sigil::create_glyph_atlas(GlyphEngine& engine) { max_row_height = std::max(max_row_height, glyph.metric.height); } - // Transfer atlas_data to GPU - std::cout << "Sigil: Uploading atlas to GPU..." << std::endl; VkBuffer staging_buffer; VkDeviceMemory staging_buffer_memory; - - std::cout << "Sigil: Creating staging buffer..." << std::endl; + VkBufferCreateInfo buffer_info{}; buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; @@ -1034,11 +1026,9 @@ void Sigil::create_glyph_atlas(GlyphEngine& engine) { std::println(stderr, "Sigil: Failed to create staging buffer"); return; } - std::cout << "Sigil: Staging buffer created" << std::endl; + VkMemoryRequirements mem_reqs_buf; - std::cout << "Sigil: Getting buffer memory requirements..." << std::endl; vkGetBufferMemoryRequirements(device_, staging_buffer, &mem_reqs_buf); - std::cout << "Sigil: Buffer memory requirements: " << mem_reqs_buf.size << std::endl; VkMemoryAllocateInfo alloc_info_buf{}; alloc_info_buf.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; @@ -1131,7 +1121,6 @@ void Sigil::create_glyph_atlas(GlyphEngine& engine) { void Sigil::initialize_assets(GlyphEngine& engine) { create_glyph_atlas(engine); create_descriptor_set(); - std::cout << "Sigil: Assets and descriptors initialized" << std::endl; } uint32_t Sigil::find_memory_type(uint32_t typeFilter, VkMemoryPropertyFlags properties) { @@ -1195,6 +1184,7 @@ void Sigil::create_descriptor_set() { alloc_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; alloc_info.descriptorPool = descriptor_pool_; alloc_info.descriptorSetCount = 1; + alloc_info.pSetLayouts = &descriptor_set_layout_; if (vkAllocateDescriptorSets(device_, &alloc_info, &descriptor_set_) != VK_SUCCESS) { std::println(stderr, "Sigil: Failed to allocate descriptor set"); From 5e0cc653689fb2138ea0112aee65d71bcf8da210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 10:16:55 +0000 Subject: [PATCH 02/17] fix: address Devin Review issues and CI build failure - Add missing include in nexus.cpp (fixes CI: std::isdigit) - Add wl_display_flush() to event loop (outgoing requests reach compositor) - Return bool from prepare_read() and handle failure (dispatch_pending loop) - Cancel Wayland read lock on poll error before breaking - Move vkResetFences after successful vkAcquireNextImageKHR (prevents deadlock) - Free old command buffers before re-allocation in record_command_buffers() - Fix shell fallback comment accuracy --- include/sigil.hpp | 3 ++- src/loom.cpp | 2 +- src/main.cpp | 17 +++++++++++------ src/nexus.cpp | 1 + src/sigil.cpp | 11 +++++++++-- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/include/sigil.hpp b/include/sigil.hpp index ae57d81..0e22804 100644 --- a/include/sigil.hpp +++ b/include/sigil.hpp @@ -74,9 +74,10 @@ namespace Kaelum { */ void poll_events(); - void prepare_read() { wl_display_prepare_read(display_); } + bool prepare_read() { return wl_display_prepare_read(display_) == 0; } void cancel_read() { wl_display_cancel_read(display_); } void dispatch_pending() { wl_display_dispatch_pending(display_); } + void flush() { wl_display_flush(display_); } /** * @brief Returns the Wayland display file descriptor. diff --git a/src/loom.cpp b/src/loom.cpp index 99477c4..0aa38b3 100644 --- a/src/loom.cpp +++ b/src/loom.cpp @@ -34,7 +34,7 @@ std::expected Loom::initialize() { } if (child_pid_ == 0) { - // Child: Execute the user's preferred shell, with Fish as first preference + // Child: Execute user's preferred shell ($SHELL), fall back to fish, then /bin/sh const char* shell = std::getenv("SHELL"); if (!shell || shell[0] == '\0') shell = "/usr/bin/fish"; execl(shell, shell, nullptr); diff --git a/src/main.cpp b/src/main.cpp index 2290979..2484984 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -71,13 +71,19 @@ int main() { }; while (true) { - // 1. Prepare Wayland for reading - sigil.prepare_read(); - - // Poll for events + sigil.dispatch_pending(); + sigil.flush(); + + if (!sigil.prepare_read()) { + sigil.dispatch_pending(); + sigil.render(nexus); + continue; + } + int ret = poll(poll_fds.data(), poll_fds.size(), 1); - + if (ret < 0) { + sigil.cancel_read(); std::println(stderr, "Poll error."); break; } @@ -86,7 +92,6 @@ int main() { sigil.poll_events(); } else { sigil.cancel_read(); - sigil.dispatch_pending(); } if (poll_fds[1].revents & POLLIN) { diff --git a/src/nexus.cpp b/src/nexus.cpp index 66156f0..cf7fe3e 100644 --- a/src/nexus.cpp +++ b/src/nexus.cpp @@ -1,5 +1,6 @@ #include "nexus.hpp" #include +#include #include namespace Kaelum { diff --git a/src/sigil.cpp b/src/sigil.cpp index a0c07cf..ab767ff 100644 --- a/src/sigil.cpp +++ b/src/sigil.cpp @@ -696,19 +696,20 @@ void Sigil::render(const Nexus& nexus) { if (swapchain_ == VK_NULL_HANDLE) return; vkWaitForFences(device_, 1, &in_flight_fence_, VK_TRUE, UINT64_MAX); - vkResetFences(device_, 1, &in_flight_fence_); uint32_t image_index; VkResult result = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX, image_available_semaphore_, VK_NULL_HANDLE, &image_index); if (result == VK_ERROR_OUT_OF_DATE_KHR) { - on_resize(0, 0); + on_resize(0, 0); return; } else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { std::println(stderr, "Sigil: Failed to acquire swapchain image"); return; } + vkResetFences(device_, 1, &in_flight_fence_); + const auto& grid = nexus.get_grid(); std::vector vertices; @@ -866,6 +867,12 @@ void Sigil::create_command_pool() { } void Sigil::record_command_buffers() { + if (!command_buffers_.empty()) { + vkFreeCommandBuffers(device_, command_pool_, + static_cast(command_buffers_.size()), command_buffers_.data()); + command_buffers_.clear(); + } + uint32_t image_count = static_cast(swapchain_images_.size()); command_buffers_.resize(image_count); From 6fd028bfe5dd5a1d1885327b52141cb500a7f212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 10:18:36 +0000 Subject: [PATCH 03/17] fix: clean up build warnings - Add -O1 to debug flags (satisfies _FORTIFY_SOURCE=3 requirement) - Suppress unused parameter warnings in xdg_surface_handle_configure, parse_sgr - Remove unused current_frame_ member --- CMakeLists.txt | 2 +- include/sigil.hpp | 1 - src/nexus.cpp | 4 +--- src/sigil.cpp | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e70b16..88b8a4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,7 +49,7 @@ set(COMMON_FLAGS "-march=${KAELUM_ARCH} -pipe -fno-plt -fstack-protector-strong -fstack-clash-protection -D_FORTIFY_SOURCE=3 -fPIE -flto=thin -fvisibility=hidden \ -fexperimental-library -std=c++26") -set(DEBUG_FLAGS "-g -fstandalone-debug -fno-omit-frame-pointer") +set(DEBUG_FLAGS "-g -O1 -fstandalone-debug -fno-omit-frame-pointer") set(RELEASE_FLAGS "-O3 -DNDEBUG") set(CMAKE_CXX_FLAGS "${COMMON_FLAGS} ${DEBUG_FLAGS}" CACHE STRING "" FORCE) diff --git a/include/sigil.hpp b/include/sigil.hpp index 0e22804..6f1efc8 100644 --- a/include/sigil.hpp +++ b/include/sigil.hpp @@ -130,7 +130,6 @@ namespace Kaelum { VkSemaphore image_available_semaphore_ = VK_NULL_HANDLE; VkSemaphore render_finished_semaphore_ = VK_NULL_HANDLE; VkFence in_flight_fence_ = VK_NULL_HANDLE; - uint32_t current_frame_ = 0; // Vertex Buffer VkBuffer vertex_buffer_ = VK_NULL_HANDLE; diff --git a/src/nexus.cpp b/src/nexus.cpp index cf7fe3e..02b9d5e 100644 --- a/src/nexus.cpp +++ b/src/nexus.cpp @@ -115,9 +115,7 @@ void Nexus::process_csi(uint8_t final_char) { } } -void Nexus::parse_sgr(std::span params) { - // Simplified SGR parsing for colors - // In a real implementation, this would handle 256-color and TrueColor +void Nexus::parse_sgr(std::span /*params*/) { } void Nexus::move_cursor(int dx, int dy) { diff --git a/src/sigil.cpp b/src/sigil.cpp index ab767ff..0161d27 100644 --- a/src/sigil.cpp +++ b/src/sigil.cpp @@ -114,7 +114,7 @@ static const struct wl_registry_listener registry_listener = { }; // XDG Surface Helpers -static void xdg_surface_handle_configure(void* data, struct xdg_surface* xdg_surface, uint32_t serial) { +static void xdg_surface_handle_configure(void* /*data*/, struct xdg_surface* xdg_surface, uint32_t serial) { std::println("Sigil: Received xdg_surface configure event (serial: {})", serial); xdg_surface_ack_configure(xdg_surface, serial); } From 08855c6a729be76c54a5c5773c120b8484a5d928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 10:21:35 +0000 Subject: [PATCH 04/17] fix: Hyprland/wlroots swapchain creation and SIGSEGV crash - Use configured dimensions from xdg_toplevel configure event when Vulkan reports UINT32_MAX extent (standard wlroots behavior) - Defer resize handling until after initial Vulkan init is complete (prevents SIGSEGV when configure fires during init_wayland roundtrip) - Query supportedCompositeAlpha from surface capabilities instead of hardcoding VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR (not supported on many Hyprland/wlroots setups) - Guard against zero-extent swapchain creation --- include/sigil.hpp | 8 ++++++++ src/sigil.cpp | 46 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/include/sigil.hpp b/include/sigil.hpp index 6f1efc8..6263267 100644 --- a/include/sigil.hpp +++ b/include/sigil.hpp @@ -90,6 +90,11 @@ namespace Kaelum { */ void on_resize(uint32_t width, uint32_t height); + /** + * @brief Called from xdg_toplevel configure to store dimensions. + */ + void handle_configure(uint32_t width, uint32_t height); + private: friend void registry_handle_global(void* data, struct wl_registry* registry, uint32_t id, const char* interface, uint32_t version); @@ -112,6 +117,9 @@ namespace Kaelum { VkSurfaceKHR vk_surface_ = VK_NULL_HANDLE; VkSwapchainKHR swapchain_ = VK_NULL_HANDLE; VkExtent2D extent_ = {0, 0}; + uint32_t configured_width_ = 800; + uint32_t configured_height_ = 600; + bool initial_configure_done_ = false; std::vector swapchain_images_; std::vector swapchain_image_views_; std::vector swapchain_framebuffers_; diff --git a/src/sigil.cpp b/src/sigil.cpp index 0161d27..cf80b1e 100644 --- a/src/sigil.cpp +++ b/src/sigil.cpp @@ -87,6 +87,7 @@ std::expected Sigil::initialize() { auto l_res = init_level_zero(); if (!l_res) return std::unexpected(l_res.error()); + initial_configure_done_ = true; std::println("Sigil: Full hardware pipeline initialized successfully."); return {}; } @@ -128,9 +129,8 @@ static void xdg_toplevel_handle_configure(void* data, struct xdg_toplevel* xdg_t (void)xdg_toplevel; (void)states; Sigil* sigil = static_cast(data); - if (width > 0 && height > 0) { - sigil->on_resize(static_cast(width), static_cast(height)); - } + sigil->handle_configure(width > 0 ? static_cast(width) : 0, + height > 0 ? static_cast(height) : 0); } static const struct xdg_toplevel_listener xdg_toplevel_listener = { @@ -328,9 +328,7 @@ std::expected Sigil::create_swapchain() { VkSurfaceCapabilitiesKHR capabilities{}; vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device_, vk_surface_, &capabilities); - std::println("Sigil: Capabilities extent: {}x{}", capabilities.currentExtent.width, capabilities.currentExtent.height); if (capabilities.currentExtent.width == 0 || capabilities.currentExtent.height == 0) { - std::println(stderr, "Sigil: Invalid capabilities extent."); return std::unexpected(SigilError::AllocationFailed); } @@ -373,9 +371,12 @@ std::expected Sigil::create_swapchain() { VkExtent2D actualExtent = capabilities.currentExtent; if (actualExtent.width == UINT32_MAX) { - actualExtent = {800, 600}; - actualExtent.width = std::clamp(actualExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); - actualExtent.height = std::clamp(actualExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height); + actualExtent.width = std::clamp(configured_width_, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); + actualExtent.height = std::clamp(configured_height_, capabilities.minImageExtent.height, capabilities.maxImageExtent.height); + } + + if (actualExtent.width == 0 || actualExtent.height == 0) { + return std::unexpected(SigilError::AllocationFailed); } VkSwapchainCreateInfoKHR create_info{}; @@ -392,7 +393,18 @@ std::expected Sigil::create_swapchain() { create_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; create_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; create_info.preTransform = capabilities.currentTransform; - create_info.compositeAlpha = VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR; + + VkCompositeAlphaFlagBitsKHR composite_alpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + for (auto flag : {VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, + VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR, + VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR, + VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR}) { + if (capabilities.supportedCompositeAlpha & flag) { + composite_alpha = flag; + break; + } + } + create_info.compositeAlpha = composite_alpha; create_info.presentMode = present_mode; create_info.clipped = VK_TRUE; @@ -841,10 +853,22 @@ void Sigil::render(const Nexus& nexus) { } } +void Sigil::handle_configure(uint32_t width, uint32_t height) { + if (width > 0 && height > 0) { + configured_width_ = width; + configured_height_ = height; + } + if (initial_configure_done_) { + on_resize(configured_width_, configured_height_); + } +} + void Sigil::on_resize(uint32_t width, uint32_t height) { - (void)width; - (void)height; if (device_ == VK_NULL_HANDLE) return; + if (width > 0 && height > 0) { + configured_width_ = width; + configured_height_ = height; + } vkDeviceWaitIdle(device_); cleanup_swapchain(); if (!create_swapchain()) { From fe638a5a8d484be9f91541c0b4f103bda1058586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 10:24:50 +0000 Subject: [PATCH 05/17] fix: Hyprland swapchain recreation crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root causes: - on_resize was called synchronously inside the Wayland configure handler, but the compositor hadn't processed the surface commit yet, so vkGetPhysicalDeviceSurfaceCapabilitiesKHR still returned stale data causing swapchain creation to fail - After failed recreation, swapchain_ still held the old (destroyed) handle, so render() tried to use it → SIGSEGV - Old swapchain was not destroyed before creating new one in on_resize Fixes: - Defer resize: configure handler sets needs_resize_ flag; main loop calls process_pending_resize() after event dispatch (same pattern as Ghostty/Kitty) - Destroy old swapchain in on_resize before creating replacement - Commit wl_surface in xdg_surface_handle_configure after ack - Query supportedCompositeAlpha (OPAQUE preferred) - Add diagnostic VkResult/extent/format to swapchain failure message --- include/sigil.hpp | 7 +++++++ src/main.cpp | 4 +++- src/sigil.cpp | 26 +++++++++++++++++++++----- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/include/sigil.hpp b/include/sigil.hpp index 6263267..a338cca 100644 --- a/include/sigil.hpp +++ b/include/sigil.hpp @@ -83,6 +83,7 @@ namespace Kaelum { * @brief Returns the Wayland display file descriptor. */ int get_display_fd() const { return wl_display_get_fd(display_); } + WaylandSurface* get_wl_surface() const { return wl_surface_; } /** @@ -95,6 +96,11 @@ namespace Kaelum { */ void handle_configure(uint32_t width, uint32_t height); + /** + * @brief Process any pending resize (deferred from configure events). + */ + void process_pending_resize(); + private: friend void registry_handle_global(void* data, struct wl_registry* registry, uint32_t id, const char* interface, uint32_t version); @@ -120,6 +126,7 @@ namespace Kaelum { uint32_t configured_width_ = 800; uint32_t configured_height_ = 600; bool initial_configure_done_ = false; + bool needs_resize_ = false; std::vector swapchain_images_; std::vector swapchain_image_views_; std::vector swapchain_framebuffers_; diff --git a/src/main.cpp b/src/main.cpp index 2484984..bbc945a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -76,6 +76,7 @@ int main() { if (!sigil.prepare_read()) { sigil.dispatch_pending(); + sigil.process_pending_resize(); sigil.render(nexus); continue; } @@ -110,7 +111,8 @@ int main() { } } } - + + sigil.process_pending_resize(); sigil.render(nexus); } diff --git a/src/sigil.cpp b/src/sigil.cpp index cf80b1e..89afecf 100644 --- a/src/sigil.cpp +++ b/src/sigil.cpp @@ -115,9 +115,12 @@ static const struct wl_registry_listener registry_listener = { }; // XDG Surface Helpers -static void xdg_surface_handle_configure(void* /*data*/, struct xdg_surface* xdg_surface, uint32_t serial) { - std::println("Sigil: Received xdg_surface configure event (serial: {})", serial); +static void xdg_surface_handle_configure(void* data, struct xdg_surface* xdg_surface, uint32_t serial) { xdg_surface_ack_configure(xdg_surface, serial); + Sigil* sigil = static_cast(data); + if (sigil->get_wl_surface()) { + wl_surface_commit(sigil->get_wl_surface()); + } } static const struct xdg_surface_listener xdg_surface_listener = { @@ -408,8 +411,11 @@ std::expected Sigil::create_swapchain() { create_info.presentMode = present_mode; create_info.clipped = VK_TRUE; - if (vkCreateSwapchainKHR(device_, &create_info, nullptr, &swapchain_) != VK_SUCCESS) { - std::println(stderr, "Sigil: Failed to create swapchain"); + VkResult sc_result = vkCreateSwapchainKHR(device_, &create_info, nullptr, &swapchain_); + if (sc_result != VK_SUCCESS) { + std::println(stderr, "Sigil: Failed to create swapchain (VkResult: {}, extent: {}x{}, format: {}, composite: {})", + static_cast(sc_result), actualExtent.width, actualExtent.height, + static_cast(surface_format.format), static_cast(composite_alpha)); return std::unexpected(SigilError::AllocationFailed); } @@ -859,10 +865,16 @@ void Sigil::handle_configure(uint32_t width, uint32_t height) { configured_height_ = height; } if (initial_configure_done_) { - on_resize(configured_width_, configured_height_); + needs_resize_ = true; } } +void Sigil::process_pending_resize() { + if (!needs_resize_) return; + needs_resize_ = false; + on_resize(configured_width_, configured_height_); +} + void Sigil::on_resize(uint32_t width, uint32_t height) { if (device_ == VK_NULL_HANDLE) return; if (width > 0 && height > 0) { @@ -871,6 +883,10 @@ void Sigil::on_resize(uint32_t width, uint32_t height) { } vkDeviceWaitIdle(device_); cleanup_swapchain(); + if (swapchain_ != VK_NULL_HANDLE) { + vkDestroySwapchainKHR(device_, swapchain_, nullptr); + swapchain_ = VK_NULL_HANDLE; + } if (!create_swapchain()) { std::println(stderr, "Sigil: Failed to recreate swapchain during resize"); return; From 8dfdaa9ba3f232ec63bf1140c45d94c253fd9c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 10:31:44 +0000 Subject: [PATCH 06/17] fix: io_uring PTY I/O, keyboard input, and swapchain recreation io_uring fixes (root cause of blank window): - Use offset -1 for PTY read/write (PTYs are non-seekable; offset 0 caused -ESPIPE, so shell output never reached Nexus) - Tag SQEs with URING_TAG_READ/WRITE to distinguish completions - write() no longer blocks with io_uring_wait_cqe (was consuming read CQEs, causing terminal to go silent after first keypress) - poll_read() iterates CQ ring, consuming write CQEs and returning on the first read CQE Keyboard: - Added numbers, symbols, backspace, tab, escape to key map - Added backspace/DEL handling in Nexus (cursor_x_--) - Filter other control characters < 0x20 in ground state Swapchain recreation: - Pass oldSwapchain to vkCreateSwapchainKHR on resize (Vulkan spec allows driver to reuse resources) - Destroy old swapchain only after new one is created successfully --- include/loom.hpp | 4 +-- include/sigil.hpp | 2 +- src/loom.cpp | 73 ++++++++++++++++++++++------------------------- src/main.cpp | 21 ++++++++++---- src/nexus.cpp | 4 +++ src/sigil.cpp | 18 ++++++++---- 6 files changed, 68 insertions(+), 54 deletions(-) diff --git a/include/loom.hpp b/include/loom.hpp index 2967feb..fccaa6d 100644 --- a/include/loom.hpp +++ b/include/loom.hpp @@ -57,12 +57,12 @@ namespace Kaelum { std::expected register_wake_fd(); private: + void submit_read(); + int master_fd_ = -1; pid_t child_pid_ = -1; struct io_uring ring_; bool initialized_ = false; - - // Buffer for io_uring read requests static constexpr size_t k_ring_buffer_size = 4096; uint8_t read_buffer_[k_ring_buffer_size]; diff --git a/include/sigil.hpp b/include/sigil.hpp index a338cca..e8a4949 100644 --- a/include/sigil.hpp +++ b/include/sigil.hpp @@ -170,7 +170,7 @@ namespace Kaelum { std::expected init_wayland(); std::expected init_vulkan(); std::expected init_level_zero(); - std::expected create_swapchain(); + std::expected create_swapchain(VkSwapchainKHR old_swapchain = VK_NULL_HANDLE); void create_framebuffers(); void create_render_pass(); void create_graphics_pipeline(); diff --git a/src/loom.cpp b/src/loom.cpp index 0aa38b3..6726e42 100644 --- a/src/loom.cpp +++ b/src/loom.cpp @@ -7,6 +7,11 @@ #include #include +namespace { + constexpr uint64_t URING_TAG_READ = 1; + constexpr uint64_t URING_TAG_WRITE = 2; +} + namespace Kaelum { Loom::Loom() { @@ -54,14 +59,16 @@ std::expected Loom::initialize() { int flags = fcntl(master_fd_, F_GETFL, 0); fcntl(master_fd_, F_SETFL, flags | O_NONBLOCK); - // Submit initial read request - struct io_uring_sqe *sqe = io_uring_get_sqe(&ring_); - if (!sqe) return std::unexpected(LoomError::ReadFailed); + submit_read(); + return {}; +} - io_uring_prep_read(sqe, master_fd_, read_buffer_, k_ring_buffer_size, 0); +void Loom::submit_read() { + struct io_uring_sqe *sqe = io_uring_get_sqe(&ring_); + if (!sqe) return; + io_uring_prep_read(sqe, master_fd_, read_buffer_, k_ring_buffer_size, -1); + io_uring_sqe_set_data64(sqe, URING_TAG_READ); io_uring_submit(&ring_); - - return {}; } std::expected Loom::register_wake_fd() { @@ -78,53 +85,41 @@ std::expected Loom::register_wake_fd() { std::expected Loom::poll_read(std::span buffer) { struct io_uring_cqe *cqe; - - // Non-blocking peek at the completion queue - if (io_uring_peek_cqe(&ring_, &cqe) == 0) { - int res = cqe->res; - io_uring_cqe_seen(&ring_, cqe); - - if (res < 0) { - return std::unexpected(LoomError::ReadFailed); - } - if (res > 0) { + unsigned head; + unsigned count = 0; + io_uring_for_each_cqe(&ring_, head, cqe) { + uint64_t tag = io_uring_cqe_get_data64(cqe); + int res = cqe->res; + count++; + + if (tag == URING_TAG_READ) { + io_uring_cq_advance(&ring_, count); + if (res <= 0) { + submit_read(); + return (res == 0) ? std::expected(0) + : std::unexpected(LoomError::ReadFailed); + } size_t bytes_to_copy = std::min(static_cast(res), buffer.size()); std::memcpy(buffer.data(), read_buffer_, bytes_to_copy); - - // Re-submit read request - struct io_uring_sqe *sqe = io_uring_get_sqe(&ring_); - if (sqe) { - io_uring_prep_read(sqe, master_fd_, read_buffer_, k_ring_buffer_size, 0); - io_uring_submit(&ring_); - } - + submit_read(); return bytes_to_copy; } + // URING_TAG_WRITE completions are silently consumed } + if (count > 0) io_uring_cq_advance(&ring_, count); - return 0; // No data available + return 0; } std::expected Loom::write(std::span data) { struct io_uring_sqe *sqe = io_uring_get_sqe(&ring_); if (!sqe) return std::unexpected(LoomError::WriteFailed); - io_uring_prep_write(sqe, master_fd_, data.data(), data.size(), 0); + io_uring_prep_write(sqe, master_fd_, data.data(), data.size(), -1); + io_uring_sqe_set_data64(sqe, URING_TAG_WRITE); io_uring_submit(&ring_); - - // For writes in a terminal, we typically want to ensure they are sent - // We'll wait for this specific CQE to avoid overfilling the ring - struct io_uring_cqe *cqe; - if (io_uring_wait_cqe(&ring_, &cqe) < 0) { - return std::unexpected(LoomError::WriteFailed); - } - - int res = cqe->res; - io_uring_cqe_seen(&ring_, cqe); - - if (res < 0) return std::unexpected(LoomError::WriteFailed); - + return {}; } diff --git a/src/main.cpp b/src/main.cpp index bbc945a..f1a4b66 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -35,16 +35,25 @@ int main() { sigil.initialize_assets(glyphs); - // Map wayland keycodes to ASCII + // Map Linux input scancodes to ASCII std::map key_map = { - {30, 'a'}, {48, 'b'}, {46, 'c'}, {32, 'd'}, {18, 'e'}, {33, 'f'}, {34, 'g'}, {35, 'h'}, - {23, 'i'}, {36, 'j'}, {37, 'k'}, {38, 'l'}, {50, 'm'}, {49, 'n'}, {24, 'o'}, {25, 'p'}, - {16, 'q'}, {19, 'r'}, {31, 's'}, {20, 't'}, {22, 'u'}, {47, 'v'}, {17, 'w'}, {45, 'x'}, - {21, 'y'}, {44, 'z'}, {28, '\n'}, {57, ' '} + // Letters + {30, 'a'}, {48, 'b'}, {46, 'c'}, {32, 'd'}, {18, 'e'}, {33, 'f'}, {34, 'g'}, {35, 'h'}, + {23, 'i'}, {36, 'j'}, {37, 'k'}, {38, 'l'}, {50, 'm'}, {49, 'n'}, {24, 'o'}, {25, 'p'}, + {16, 'q'}, {19, 'r'}, {31, 's'}, {20, 't'}, {22, 'u'}, {47, 'v'}, {17, 'w'}, {45, 'x'}, + {21, 'y'}, {44, 'z'}, + // Numbers + {2, '1'}, {3, '2'}, {4, '3'}, {5, '4'}, {6, '5'}, {7, '6'}, {8, '7'}, {9, '8'}, {10, '9'}, {11, '0'}, + // Symbols + {12, '-'}, {13, '='}, {26, '['}, {27, ']'}, {39, ';'}, {40, '\''}, {41, '`'}, + {43, '\\'}, {51, ','}, {52, '.'}, {53, '/'}, + // Control + {28, '\n'}, {57, ' '}, {14, '\b'}, {15, '\t'}, {1, '\x1b'}, }; sigil.set_keyboard_callback([&loom, &key_map](uint32_t key, bool pressed) { - if (pressed && key_map.contains(key)) { + if (!pressed) return; + if (key_map.contains(key)) { char c = key_map[key]; (void)loom.write({(uint8_t*)&c, 1}); } diff --git a/src/nexus.cpp b/src/nexus.cpp index 02b9d5e..afe72bd 100644 --- a/src/nexus.cpp +++ b/src/nexus.cpp @@ -25,6 +25,10 @@ void Nexus::handle_ground(uint8_t c) { cursor_x_ = 0; } else if (c == '\t') { cursor_x_ = (cursor_x_ + 8) % k_default_cols; + } else if (c == '\b' || c == 0x7f) { + if (cursor_x_ > 0) cursor_x_--; + } else if (c < 0x20) { + // Ignore other control characters } else { set_cell(static_cast(c), vz_fg, vz_bg, 0); cursor_x_ = (cursor_x_ + 1) % k_default_cols; diff --git a/src/sigil.cpp b/src/sigil.cpp index 89afecf..f1f5369 100644 --- a/src/sigil.cpp +++ b/src/sigil.cpp @@ -327,7 +327,7 @@ std::expected Sigil::init_level_zero() { return {}; } -std::expected Sigil::create_swapchain() { +std::expected Sigil::create_swapchain(VkSwapchainKHR old_swapchain) { VkSurfaceCapabilitiesKHR capabilities{}; vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device_, vk_surface_, &capabilities); @@ -410,6 +410,7 @@ std::expected Sigil::create_swapchain() { create_info.compositeAlpha = composite_alpha; create_info.presentMode = present_mode; create_info.clipped = VK_TRUE; + create_info.oldSwapchain = old_swapchain; VkResult sc_result = vkCreateSwapchainKHR(device_, &create_info, nullptr, &swapchain_); if (sc_result != VK_SUCCESS) { @@ -883,14 +884,19 @@ void Sigil::on_resize(uint32_t width, uint32_t height) { } vkDeviceWaitIdle(device_); cleanup_swapchain(); - if (swapchain_ != VK_NULL_HANDLE) { - vkDestroySwapchainKHR(device_, swapchain_, nullptr); - swapchain_ = VK_NULL_HANDLE; - } - if (!create_swapchain()) { + + VkSwapchainKHR old_swapchain = swapchain_; + swapchain_ = VK_NULL_HANDLE; + + if (!create_swapchain(old_swapchain)) { std::println(stderr, "Sigil: Failed to recreate swapchain during resize"); + if (old_swapchain != VK_NULL_HANDLE) + vkDestroySwapchainKHR(device_, old_swapchain, nullptr); return; } + if (old_swapchain != VK_NULL_HANDLE) + vkDestroySwapchainKHR(device_, old_swapchain, nullptr); + create_framebuffers(); record_command_buffers(); } From a15b07e39d241e2a300b7a18dcd6da889ccce263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 10:37:18 +0000 Subject: [PATCH 07/17] feat: dynamic terminal resize, TIOCSWINSZ, CSI sequences, arrow keys Dynamic resize: - Grid changed from fixed std::array to std::vector with runtime cols_/rows_ members - Nexus::resize() copies existing content to new grid dimensions - Loom::set_pty_size() sends TIOCSWINSZ to the shell on resize - Sigil fires resize_callback_ from on_resize() with cols/rows calculated from pixel extent and cell dimensions - Cell dimensions derived from FreeType max_advance and line_height Rendering adapts to dynamic grid: - render() reads nexus.cols()/rows() instead of k_default_cols/rows - Cells fill full NDC range (-1..1) for the current grid size New CSI sequences: - J modes 0/1/2/3 (erase in display: cursor-to-end, start-to-cursor, full, full+scrollback) - K modes 0/1/2 (erase in line) - L/M (insert/delete lines) - P/@ (delete/insert characters) - d (vertical line position absolute) - G (cursor character absolute) Keyboard: - Arrow keys, Home, End, PgUp, PgDn, Delete emit escape sequences - Numbers, symbols, backspace, tab, escape mapped --- include/common.hpp | 4 +- include/glyph_engine.hpp | 2 + include/loom.hpp | 9 +++ include/nexus.hpp | 7 ++- include/sigil.hpp | 16 +++++ src/glyph_engine.cpp | 1 + src/main.cpp | 37 +++++++++++- src/nexus.cpp | 127 ++++++++++++++++++++++++++++++++++----- src/sigil.cpp | 35 +++++------ 9 files changed, 199 insertions(+), 39 deletions(-) diff --git a/include/common.hpp b/include/common.hpp index 2e4baff..77cf216 100644 --- a/include/common.hpp +++ b/include/common.hpp @@ -2,7 +2,7 @@ #include #include -#include +#include namespace Kaelum { @@ -41,6 +41,6 @@ namespace Kaelum { /** * @brief Grid of cells representing the terminal state */ - using Grid = std::array; + using Grid = std::vector; } // namespace Kaelum diff --git a/include/glyph_engine.hpp b/include/glyph_engine.hpp index 61c7254..3c281b3 100644 --- a/include/glyph_engine.hpp +++ b/include/glyph_engine.hpp @@ -39,11 +39,13 @@ namespace Kaelum { std::expected get_glyph(char32_t codepoint); uint32_t get_line_height() const { return line_height_; } + uint32_t get_cell_width() const { return cell_width_; } private: FT_Library library_ = nullptr; FT_Face face_ = nullptr; uint32_t line_height_ = 0; + uint32_t cell_width_ = 0; std::map glyph_cache_; }; diff --git a/include/loom.hpp b/include/loom.hpp index fccaa6d..68c5719 100644 --- a/include/loom.hpp +++ b/include/loom.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "common.hpp" @@ -56,6 +57,14 @@ namespace Kaelum { */ std::expected register_wake_fd(); + /** + * @brief Sets the PTY window size (TIOCSWINSZ). + */ + void set_pty_size(uint16_t cols, uint16_t rows, uint16_t xpixel = 0, uint16_t ypixel = 0) { + struct winsize ws = {rows, cols, xpixel, ypixel}; + if (master_fd_ >= 0) ioctl(master_fd_, TIOCSWINSZ, &ws); + } + private: void submit_read(); diff --git a/include/nexus.hpp b/include/nexus.hpp index b781c11..d7435cb 100644 --- a/include/nexus.hpp +++ b/include/nexus.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include "common.hpp" namespace Kaelum { @@ -33,7 +32,11 @@ namespace Kaelum { std::expected process_input(std::span data); + void resize(size_t cols, size_t rows); + const Grid& get_grid() const { return grid_; } + size_t cols() const { return cols_; } + size_t rows() const { return rows_; } std::pair get_cursor() const { return {cursor_x_, cursor_y_}; } private: @@ -51,6 +54,8 @@ namespace Kaelum { void process_csi(uint8_t final_char); void parse_sgr(std::span params); + size_t cols_ = k_default_cols; + size_t rows_ = k_default_rows; Grid grid_; size_t cursor_x_ = 0; size_t cursor_y_ = 0; diff --git a/include/sigil.hpp b/include/sigil.hpp index e8a4949..6b235a2 100644 --- a/include/sigil.hpp +++ b/include/sigil.hpp @@ -69,6 +69,15 @@ namespace Kaelum { */ void set_keyboard_callback(std::function callback); + /** + * @brief Sets a callback for window resize events (cols, rows, width_px, height_px). + */ + void set_resize_callback(std::function callback) { + resize_callback_ = std::move(callback); + } + + void set_cell_size(uint32_t w, uint32_t h) { cell_width_ = w; cell_height_ = h; } + /** * @brief Polls Wayland events and updates surface state. */ @@ -162,6 +171,13 @@ namespace Kaelum { VkDescriptorPool descriptor_pool_ = VK_NULL_HANDLE; VkDescriptorSet descriptor_set_ = VK_NULL_HANDLE; + // Cell dimensions for resize calculation + uint32_t cell_width_ = 8; + uint32_t cell_height_ = 14; + + // Callbacks + std::function resize_callback_; + // Intel Level Zero handles (opaque pointers) void* lz_driver_ = nullptr; void* lz_device_ = nullptr; diff --git a/src/glyph_engine.cpp b/src/glyph_engine.cpp index ddaabd5..ce728c0 100644 --- a/src/glyph_engine.cpp +++ b/src/glyph_engine.cpp @@ -50,6 +50,7 @@ std::expected GlyphEngine::load_font(const std::string& font_f FT_Set_Pixel_Sizes(face_, 0, 14); line_height_ = static_cast(face_->size->metrics.height >> 6); + cell_width_ = static_cast(face_->size->metrics.max_advance >> 6); std::println("GlyphEngine: Loaded font face: {}", reinterpret_cast(face_->family_name)); diff --git a/src/main.cpp b/src/main.cpp index f1a4b66..0bf0bce 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -34,7 +34,22 @@ int main() { sigil.initialize_assets(glyphs); - + // Set cell dimensions for resize calculation + uint32_t cw = glyphs.get_cell_width(); + uint32_t ch = glyphs.get_line_height(); + if (cw == 0) cw = 8; + if (ch == 0) ch = 14; + sigil.set_cell_size(cw, ch); + + sigil.set_resize_callback([&nexus, &loom](uint32_t cols, uint32_t rows, uint32_t wpx, uint32_t hpx) { + nexus.resize(cols, rows); + loom.set_pty_size(static_cast(cols), static_cast(rows), + static_cast(wpx), static_cast(hpx)); + }); + + // Set initial PTY size + loom.set_pty_size(static_cast(k_default_cols), static_cast(k_default_rows)); + // Map Linux input scancodes to ASCII std::map key_map = { // Letters @@ -51,9 +66,25 @@ int main() { {28, '\n'}, {57, ' '}, {14, '\b'}, {15, '\t'}, {1, '\x1b'}, }; - sigil.set_keyboard_callback([&loom, &key_map](uint32_t key, bool pressed) { + // Arrow keys and special keys → escape sequences + std::map special_key_map = { + {103, "\x1b[A"}, // Up + {108, "\x1b[B"}, // Down + {106, "\x1b[C"}, // Right + {105, "\x1b[D"}, // Left + {102, "\x1b[H"}, // Home + {107, "\x1b[F"}, // End + {104, "\x1b[5~"}, // Page Up + {109, "\x1b[6~"}, // Page Down + {111, "\x1b[3~"}, // Delete + }; + + sigil.set_keyboard_callback([&loom, &key_map, &special_key_map](uint32_t key, bool pressed) { if (!pressed) return; - if (key_map.contains(key)) { + if (special_key_map.contains(key)) { + const auto& seq = special_key_map[key]; + (void)loom.write({(const uint8_t*)seq.data(), seq.size()}); + } else if (key_map.contains(key)) { char c = key_map[key]; (void)loom.write({(uint8_t*)&c, 1}); } diff --git a/src/nexus.cpp b/src/nexus.cpp index afe72bd..edcbcb3 100644 --- a/src/nexus.cpp +++ b/src/nexus.cpp @@ -6,8 +6,8 @@ namespace Kaelum { Nexus::Nexus() { - grid_.fill(Cell{}); - + grid_.resize(cols_ * rows_, Cell{}); + // O(1) Dispatch table using member function pointers dispatch_table_[static_cast(State::Ground)] = &Nexus::handle_ground; dispatch_table_[static_cast(State::Escape)] = &Nexus::handle_escape; @@ -19,21 +19,24 @@ void Nexus::handle_ground(uint8_t c) { if (c == 27) { current_state_ = State::Escape; } else if (c == '\n') { - cursor_y_ = (cursor_y_ + 1) % k_default_rows; + cursor_y_++; + if (cursor_y_ >= rows_) cursor_y_ = rows_ - 1; cursor_x_ = 0; } else if (c == '\r') { cursor_x_ = 0; } else if (c == '\t') { - cursor_x_ = (cursor_x_ + 8) % k_default_cols; + cursor_x_ = std::min(cursor_x_ + (8 - cursor_x_ % 8), cols_ - 1); } else if (c == '\b' || c == 0x7f) { if (cursor_x_ > 0) cursor_x_--; } else if (c < 0x20) { // Ignore other control characters } else { set_cell(static_cast(c), vz_fg, vz_bg, 0); - cursor_x_ = (cursor_x_ + 1) % k_default_cols; - if (cursor_x_ == 0) { - cursor_y_ = (cursor_y_ + 1) % k_default_rows; + cursor_x_++; + if (cursor_x_ >= cols_) { + cursor_x_ = 0; + cursor_y_++; + if (cursor_y_ >= rows_) cursor_y_ = rows_ - 1; } } } @@ -98,8 +101,8 @@ void Nexus::process_csi(uint8_t final_char) { { int row = params.size() > 0 ? params[0] : 1; int col = params.size() > 1 ? params[1] : 1; - cursor_x_ = std::clamp(col - 1, 0, (int)k_default_cols - 1); - cursor_y_ = std::clamp(row - 1, 0, (int)k_default_rows - 1); + cursor_x_ = std::clamp(col - 1, 0, (int)cols_ - 1); + cursor_y_ = std::clamp(row - 1, 0, (int)rows_ - 1); } break; case 'f': // Cursor Position (CUP) @@ -108,9 +111,88 @@ void Nexus::process_csi(uint8_t final_char) { case 'J': // Erase in Display (ED) { int mode = params.empty() ? 0 : params[0]; - if (mode == 0 || mode == 2) clear_screen(); + if (mode == 0) { + // Clear from cursor to end of screen + for (size_t x = cursor_x_; x < cols_; ++x) + grid_[cursor_y_ * cols_ + x] = Cell{}; + for (size_t y = cursor_y_ + 1; y < rows_; ++y) + for (size_t x = 0; x < cols_; ++x) + grid_[y * cols_ + x] = Cell{}; + } else if (mode == 1) { + // Clear from start to cursor + for (size_t y = 0; y < cursor_y_; ++y) + for (size_t x = 0; x < cols_; ++x) + grid_[y * cols_ + x] = Cell{}; + for (size_t x = 0; x <= cursor_x_; ++x) + grid_[cursor_y_ * cols_ + x] = Cell{}; + } else if (mode == 2 || mode == 3) { + clear_screen(); + } + } + break; + case 'K': // Erase in Line (EL) + { + int mode = params.empty() ? 0 : params[0]; + if (mode == 0) { + for (size_t x = cursor_x_; x < cols_; ++x) + grid_[cursor_y_ * cols_ + x] = Cell{}; + } else if (mode == 1) { + for (size_t x = 0; x <= cursor_x_; ++x) + grid_[cursor_y_ * cols_ + x] = Cell{}; + } else if (mode == 2) { + for (size_t x = 0; x < cols_; ++x) + grid_[cursor_y_ * cols_ + x] = Cell{}; + } + } + break; + case 'L': // Insert Lines + { + int n = params.empty() ? 1 : params[0]; + for (int i = 0; i < n && cursor_y_ + 1 < rows_; ++i) { + for (size_t y = rows_ - 1; y > cursor_y_; --y) + for (size_t x = 0; x < cols_; ++x) + grid_[y * cols_ + x] = grid_[(y - 1) * cols_ + x]; + for (size_t x = 0; x < cols_; ++x) + grid_[cursor_y_ * cols_ + x] = Cell{}; + } + } + break; + case 'M': // Delete Lines + { + int n = params.empty() ? 1 : params[0]; + for (int i = 0; i < n && cursor_y_ < rows_; ++i) { + for (size_t y = cursor_y_; y + 1 < rows_; ++y) + for (size_t x = 0; x < cols_; ++x) + grid_[y * cols_ + x] = grid_[(y + 1) * cols_ + x]; + for (size_t x = 0; x < cols_; ++x) + grid_[(rows_ - 1) * cols_ + x] = Cell{}; + } } break; + case 'P': // Delete Characters + { + int n = params.empty() ? 1 : params[0]; + for (size_t x = cursor_x_; x + n < cols_; ++x) + grid_[cursor_y_ * cols_ + x] = grid_[cursor_y_ * cols_ + x + n]; + for (size_t x = (cols_ > static_cast(n) ? cols_ - n : 0); x < cols_; ++x) + grid_[cursor_y_ * cols_ + x] = Cell{}; + } + break; + case '@': // Insert Characters + { + int n = params.empty() ? 1 : params[0]; + for (size_t x = cols_ - 1; x >= cursor_x_ + n; --x) + grid_[cursor_y_ * cols_ + x] = grid_[cursor_y_ * cols_ + x - n]; + for (size_t x = cursor_x_; x < cursor_x_ + static_cast(n) && x < cols_; ++x) + grid_[cursor_y_ * cols_ + x] = Cell{}; + } + break; + case 'd': // Vertical Line Position Absolute (VPA) + cursor_y_ = std::clamp((params.empty() ? 1 : params[0]) - 1, 0, (int)rows_ - 1); + break; + case 'G': // Cursor Character Absolute (CHA) + cursor_x_ = std::clamp((params.empty() ? 1 : params[0]) - 1, 0, (int)cols_ - 1); + break; case 'm': // Select Graphic Rendition (SGR) parse_sgr(sequence_buffer_); break; @@ -122,17 +204,34 @@ void Nexus::process_csi(uint8_t final_char) { void Nexus::parse_sgr(std::span /*params*/) { } +void Nexus::resize(size_t cols, size_t rows) { + if (cols == 0 || rows == 0) return; + Grid new_grid(cols * rows, Cell{}); + size_t copy_cols = std::min(cols, cols_); + size_t copy_rows = std::min(rows, rows_); + for (size_t y = 0; y < copy_rows; ++y) { + for (size_t x = 0; x < copy_cols; ++x) { + new_grid[y * cols + x] = grid_[y * cols_ + x]; + } + } + grid_ = std::move(new_grid); + cols_ = cols; + rows_ = rows; + cursor_x_ = std::min(cursor_x_, cols_ - 1); + cursor_y_ = std::min(cursor_y_, rows_ - 1); +} + void Nexus::move_cursor(int dx, int dy) { - cursor_x_ = std::clamp(static_cast(cursor_x_ + dx), size_t(0), k_default_cols - 1); - cursor_y_ = std::clamp(static_cast(cursor_y_ + dy), size_t(0), k_default_rows - 1); + cursor_x_ = std::clamp(static_cast(cursor_x_ + dx), size_t(0), cols_ - 1); + cursor_y_ = std::clamp(static_cast(cursor_y_ + dy), size_t(0), rows_ - 1); } void Nexus::set_cell(char32_t cp, Color fg, Color bg, uint32_t attrs) { - grid_[cursor_y_ * k_default_cols + cursor_x_] = Cell{cp, fg, bg, attrs}; + grid_[cursor_y_ * cols_ + cursor_x_] = Cell{cp, fg, bg, attrs}; } void Nexus::clear_screen() { - grid_.fill(Cell{}); + std::fill(grid_.begin(), grid_.end(), Cell{}); cursor_x_ = 0; cursor_y_ = 0; } diff --git a/src/sigil.cpp b/src/sigil.cpp index f1f5369..7fe91bf 100644 --- a/src/sigil.cpp +++ b/src/sigil.cpp @@ -730,28 +730,18 @@ void Sigil::render(const Nexus& nexus) { vkResetFences(device_, 1, &in_flight_fence_); const auto& grid = nexus.get_grid(); + size_t n_cols = nexus.cols(); + size_t n_rows = nexus.rows(); std::vector vertices; - float window_aspect = (float)extent_.width / (float)extent_.height; - float grid_aspect = (float)k_default_cols / (float)k_default_rows; + float cell_w = 2.0f / n_cols; + float cell_h = 2.0f / n_rows; + float origin_x = -1.0f; + float origin_y = 1.0f; - float scale_x = 1.0f; - float scale_y = 1.0f; - - if (window_aspect > grid_aspect) { - scale_x = grid_aspect / window_aspect; - } else { - scale_y = window_aspect / grid_aspect; - } - - float cell_w = (2.0f * scale_x) / k_default_cols; - float cell_h = (2.0f * scale_y) / k_default_rows; - float origin_x = -scale_x; - float origin_y = scale_y; - - for (uint32_t y = 0; y < k_default_rows; ++y) { - for (uint32_t x = 0; x < k_default_cols; ++x) { - const auto& cell = grid[y * k_default_cols + x]; + for (uint32_t y = 0; y < n_rows; ++y) { + for (uint32_t x = 0; x < n_cols; ++x) { + const auto& cell = grid[y * n_cols + x]; if (cell.codepoint == U' ') continue; auto it = glyph_map_.find(cell.codepoint); @@ -899,6 +889,13 @@ void Sigil::on_resize(uint32_t width, uint32_t height) { create_framebuffers(); record_command_buffers(); + + if (resize_callback_ && cell_width_ > 0 && cell_height_ > 0) { + uint32_t cols = extent_.width / cell_width_; + uint32_t rows = extent_.height / cell_height_; + if (cols > 0 && rows > 0) + resize_callback_(cols, rows, extent_.width, extent_.height); + } } void Sigil::create_command_pool() { From 2027ca6322a2e9c92f8060347f24fdbf1bb63fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 10:39:15 +0000 Subject: [PATCH 08/17] fix: add Kaelum:: namespace qualifier to k_default_cols/rows in main.cpp --- src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 0bf0bce..548bd2e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -48,7 +48,7 @@ int main() { }); // Set initial PTY size - loom.set_pty_size(static_cast(k_default_cols), static_cast(k_default_rows)); + loom.set_pty_size(static_cast(Kaelum::k_default_cols), static_cast(Kaelum::k_default_rows)); // Map Linux input scancodes to ASCII std::map key_map = { From a8f18fcd923be3c921a0072552a385e127afd0fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 10:42:56 +0000 Subject: [PATCH 09/17] fix: Hyprland windowing compliance and frame pacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wayland protocol fixes: - Add xdg_wm_base ping/pong listener (Hyprland marks unresponsive apps that don't respond, may withhold configure events) - Handle xdg_toplevel close event (sets should_close flag, main loop exits cleanly) - Ignore 0x0 configure (compositor saying 'pick your own size') - Set xdg_toplevel_set_min_size(320,240) so compositor knows minimum - Main loop checks should_close() instead of while(true) Frame callback pacing: - After present, request wl_surface.frame callback - render() skips if frame_pending_ (waits for compositor to signal frame_done before drawing again — prevents overdriving) - Properly clean up frame_callback in destructor Also handle VK_SUBOPTIMAL_KHR on present by triggering resize --- include/sigil.hpp | 7 +++++++ src/main.cpp | 2 +- src/sigil.cpp | 46 +++++++++++++++++++++++++++++++++++++++------- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/include/sigil.hpp b/include/sigil.hpp index 6b235a2..8171fdd 100644 --- a/include/sigil.hpp +++ b/include/sigil.hpp @@ -78,6 +78,10 @@ namespace Kaelum { void set_cell_size(uint32_t w, uint32_t h) { cell_width_ = w; cell_height_ = h; } + bool should_close() const { return should_close_; } + void request_close() { should_close_ = true; } + void frame_done() { frame_pending_ = false; frame_callback_ = nullptr; } + /** * @brief Polls Wayland events and updates surface state. */ @@ -136,6 +140,9 @@ namespace Kaelum { uint32_t configured_height_ = 600; bool initial_configure_done_ = false; bool needs_resize_ = false; + bool should_close_ = false; + bool frame_pending_ = false; + struct wl_callback* frame_callback_ = nullptr; std::vector swapchain_images_; std::vector swapchain_image_views_; std::vector swapchain_framebuffers_; diff --git a/src/main.cpp b/src/main.cpp index 548bd2e..aca152e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -110,7 +110,7 @@ int main() { { wake_fd, POLLIN, 0 } }; - while (true) { + while (!sigil.should_close()) { sigil.dispatch_pending(); sigil.flush(); diff --git a/src/sigil.cpp b/src/sigil.cpp index 7fe91bf..bdb46b8 100644 --- a/src/sigil.cpp +++ b/src/sigil.cpp @@ -54,6 +54,7 @@ Sigil::~Sigil() { if (instance_ != VK_NULL_HANDLE) vkDestroyInstance(instance_, nullptr); // Wayland cleanup + if (frame_callback_) wl_callback_destroy(frame_callback_); if (keyboard_) wl_keyboard_destroy(keyboard_); if (xdg_toplevel_) xdg_toplevel_destroy(xdg_toplevel_); if (xdg_surface_) xdg_surface_destroy(xdg_surface_); @@ -102,6 +103,12 @@ void registry_handle_global(void* data, struct wl_registry* registry, uint32_t i std::println("Sigil: Bound wl_compositor"); } else if (iface == "xdg_wm_base") { sigil->xdg_wm_base_ = (struct xdg_wm_base*)wl_registry_bind(registry, id, &xdg_wm_base_interface, version); + static const struct xdg_wm_base_listener wm_base_listener = { + .ping = [](void*, struct xdg_wm_base* wm_base, uint32_t serial) { + xdg_wm_base_pong(wm_base, serial); + } + }; + xdg_wm_base_add_listener(sigil->xdg_wm_base_, &wm_base_listener, sigil); std::println("Sigil: Bound xdg_wm_base"); } else if (iface == "wl_seat") { sigil->seat_ = (struct wl_seat*)wl_registry_bind(registry, id, &wl_seat_interface, version); @@ -128,17 +135,22 @@ static const struct xdg_surface_listener xdg_surface_listener = { }; // XDG Toplevel Helpers -static void xdg_toplevel_handle_configure(void* data, struct xdg_toplevel* xdg_toplevel, int32_t width, int32_t height, struct wl_array* states) { - (void)xdg_toplevel; - (void)states; +static void xdg_toplevel_handle_configure(void* data, struct xdg_toplevel* /*xdg_toplevel*/, int32_t width, int32_t height, struct wl_array* /*states*/) { + Sigil* sigil = static_cast(data); + // width/height == 0 means compositor lets us choose; keep current configured size + if (width > 0 && height > 0) { + sigil->handle_configure(static_cast(width), static_cast(height)); + } +} + +static void xdg_toplevel_handle_close(void* data, struct xdg_toplevel* /*xdg_toplevel*/) { Sigil* sigil = static_cast(data); - sigil->handle_configure(width > 0 ? static_cast(width) : 0, - height > 0 ? static_cast(height) : 0); + sigil->request_close(); } static const struct xdg_toplevel_listener xdg_toplevel_listener = { .configure = xdg_toplevel_handle_configure, - .close = [](void*, struct xdg_toplevel*) {}, + .close = xdg_toplevel_handle_close, .configure_bounds = [](void*, struct xdg_toplevel*, int32_t, int32_t) {}, .wm_capabilities = [](void*, struct xdg_toplevel*, struct wl_array*) {}, }; @@ -187,6 +199,7 @@ std::expected Sigil::init_wayland() { xdg_toplevel_set_title(xdg_toplevel_, "Kaelum"); xdg_toplevel_set_app_id(xdg_toplevel_, "org.veridian.kaelum"); + xdg_toplevel_set_min_size(xdg_toplevel_, 320, 240); if (seat_) { keyboard_ = wl_seat_get_keyboard(seat_); @@ -711,8 +724,18 @@ void Sigil::create_sync_objects() { } } +static void frame_done_cb(void* data, struct wl_callback* callback, uint32_t /*time*/) { + wl_callback_destroy(callback); + static_cast(data)->frame_done(); +} + +static const struct wl_callback_listener frame_listener = { + .done = frame_done_cb, +}; + void Sigil::render(const Nexus& nexus) { if (swapchain_ == VK_NULL_HANDLE) return; + if (frame_pending_) return; vkWaitForFences(device_, 1, &in_flight_fence_, VK_TRUE, UINT64_MAX); @@ -845,9 +868,18 @@ void Sigil::render(const Nexus& nexus) { presentInfo.pSwapchains = &swapchain_; presentInfo.pImageIndices = &image_index; - if (vkQueuePresentKHR(graphics_queue_, &presentInfo) != VK_SUCCESS) { + VkResult present_result = vkQueuePresentKHR(graphics_queue_, &presentInfo); + if (present_result == VK_ERROR_OUT_OF_DATE_KHR || present_result == VK_SUBOPTIMAL_KHR) { + on_resize(0, 0); + } else if (present_result != VK_SUCCESS) { std::println(stderr, "Sigil: Failed to present swapchain image"); } + + // Request frame callback for vsync pacing + frame_callback_ = wl_surface_frame(wl_surface_); + wl_callback_add_listener(frame_callback_, &frame_listener, this); + wl_surface_commit(wl_surface_); + frame_pending_ = true; } void Sigil::handle_configure(uint32_t width, uint32_t height) { From 56ebab35b30005938c8fd45f0607ccef3e5b8606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 10:47:31 +0000 Subject: [PATCH 10/17] fix: xdg_surface_set_window_geometry, swapchain extent, and configure diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Wayland protocol (and Kitty's documented pattern): ack_configure → set_window_geometry → commit Without set_window_geometry, Hyprland doesn't know the app's drawing area and cannot properly tile/resize the window. Swapchain extent fixes: - Removed early bail on currentExtent == 0 (happens on Hyprland before first buffer attach, was causing swapchain creation to fail) - Fall through to use configured_width_/height_ when extent is 0 or UINT32_MAX, then clamp to driver-reported min/max Added configured_width()/configured_height() accessors so the xdg_surface configure handler can query current dimensions. Diagnostic logging in handle_configure shows compositor-sent vs stored dimensions and whether init is complete — helps debug resize chain. --- include/sigil.hpp | 2 ++ src/sigil.cpp | 26 +++++++++++++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/include/sigil.hpp b/include/sigil.hpp index 8171fdd..9a71146 100644 --- a/include/sigil.hpp +++ b/include/sigil.hpp @@ -81,6 +81,8 @@ namespace Kaelum { bool should_close() const { return should_close_; } void request_close() { should_close_ = true; } void frame_done() { frame_pending_ = false; frame_callback_ = nullptr; } + uint32_t configured_width() const { return configured_width_; } + uint32_t configured_height() const { return configured_height_; } /** * @brief Polls Wayland events and updates surface state. diff --git a/src/sigil.cpp b/src/sigil.cpp index bdb46b8..a433d86 100644 --- a/src/sigil.cpp +++ b/src/sigil.cpp @@ -123,8 +123,14 @@ static const struct wl_registry_listener registry_listener = { // XDG Surface Helpers static void xdg_surface_handle_configure(void* data, struct xdg_surface* xdg_surface, uint32_t serial) { - xdg_surface_ack_configure(xdg_surface, serial); Sigil* sigil = static_cast(data); + xdg_surface_ack_configure(xdg_surface, serial); + // Per Wayland protocol: ack → set geometry → commit + uint32_t w = sigil->configured_width(); + uint32_t h = sigil->configured_height(); + if (w > 0 && h > 0) { + xdg_surface_set_window_geometry(xdg_surface, 0, 0, w, h); + } if (sigil->get_wl_surface()) { wl_surface_commit(sigil->get_wl_surface()); } @@ -343,11 +349,7 @@ std::expected Sigil::init_level_zero() { std::expected Sigil::create_swapchain(VkSwapchainKHR old_swapchain) { VkSurfaceCapabilitiesKHR capabilities{}; vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device_, vk_surface_, &capabilities); - - if (capabilities.currentExtent.width == 0 || capabilities.currentExtent.height == 0) { - return std::unexpected(SigilError::AllocationFailed); - } - + uint32_t format_count; vkGetPhysicalDeviceSurfaceFormatsKHR(physical_device_, vk_surface_, &format_count, nullptr); if (format_count == 0) { @@ -386,9 +388,13 @@ std::expected Sigil::create_swapchain(VkSwapchainKHR old_swapc } VkExtent2D actualExtent = capabilities.currentExtent; - if (actualExtent.width == UINT32_MAX) { - actualExtent.width = std::clamp(configured_width_, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); - actualExtent.height = std::clamp(configured_height_, capabilities.minImageExtent.height, capabilities.maxImageExtent.height); + if (actualExtent.width == UINT32_MAX || actualExtent.width == 0) { + actualExtent.width = configured_width_; + actualExtent.height = configured_height_; + } + if (capabilities.maxImageExtent.width > 0) { + actualExtent.width = std::clamp(actualExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); + actualExtent.height = std::clamp(actualExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height); } if (actualExtent.width == 0 || actualExtent.height == 0) { @@ -890,6 +896,8 @@ void Sigil::handle_configure(uint32_t width, uint32_t height) { if (initial_configure_done_) { needs_resize_ = true; } + std::println("Sigil: Configure {}x{} (stored: {}x{}, init_done: {})", + width, height, configured_width_, configured_height_, initial_configure_done_); } void Sigil::process_pending_resize() { From 3786ddfcba8322f842fc078cb1b476b7bdf4fed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 10:56:11 +0000 Subject: [PATCH 11/17] fix: glyph rendering, pointer focus, scrolling, and multiple correctness bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rendering: - Glyph quads now use FreeType bearing offsets + actual bitmap dimensions for proper per-glyph positioning within cells (fixes distorted text) - GlyphRect stores full pixel metrics (bearing_x/y, width, height, advance) - GlyphMetric bearing types changed from uint32_t to int32_t (FreeType bitmap_left can be negative) - Vertex positions computed in pixel-to-NDC space using extent dimensions - Vertex buffer enlarged to 20k cells, with overflow clamping guard Wayland/Hyprland: - Added wl_pointer listener (enter/leave/motion/button/axis) so Hyprland can properly track pointer focus — fixes 'focus swaps when moving cursor' - Capped wl_seat binding to version 5 (avoids needing v8+ pointer events) - Configure handler debounced: only sets needs_resize_ when dimensions actually change Terminal: - Added scroll_up(): shifts grid up one line when cursor overflows bottom, clears last line — output no longer overwrites bottom row - Fixed move_cursor() unsigned wraparound: arithmetic now done in int before clamping, prevents cursor teleporting to far edge on negative moves I/O: - Loom::write() now copies data into persistent write_buffer_ before submitting to io_uring, fixing dangling stack pointer when callers pass temporary buffers --- include/glyph_engine.hpp | 4 +- include/loom.hpp | 2 + include/nexus.hpp | 1 + include/sigil.hpp | 5 +++ src/glyph_engine.cpp | 4 +- src/loom.cpp | 6 ++- src/nexus.cpp | 21 +++++++-- src/sigil.cpp | 92 ++++++++++++++++++++++++++-------------- 8 files changed, 95 insertions(+), 40 deletions(-) diff --git a/include/glyph_engine.hpp b/include/glyph_engine.hpp index 3c281b3..5e87556 100644 --- a/include/glyph_engine.hpp +++ b/include/glyph_engine.hpp @@ -12,8 +12,8 @@ namespace Kaelum { struct GlyphMetric { - uint32_t bearing_x; - uint32_t bearing_y; + int32_t bearing_x; + int32_t bearing_y; uint32_t width; uint32_t height; uint32_t advance; diff --git a/include/loom.hpp b/include/loom.hpp index 68c5719..853ab39 100644 --- a/include/loom.hpp +++ b/include/loom.hpp @@ -74,7 +74,9 @@ namespace Kaelum { bool initialized_ = false; static constexpr size_t k_ring_buffer_size = 4096; + static constexpr size_t k_write_buffer_size = 256; uint8_t read_buffer_[k_ring_buffer_size]; + uint8_t write_buffer_[k_write_buffer_size]; }; } // namespace Kaelum diff --git a/include/nexus.hpp b/include/nexus.hpp index d7435cb..283a958 100644 --- a/include/nexus.hpp +++ b/include/nexus.hpp @@ -63,6 +63,7 @@ namespace Kaelum { std::vector sequence_buffer_; void move_cursor(int dx, int dy); + void scroll_up(); void set_cell(char32_t cp, Color fg, Color bg, uint32_t attrs); void clear_screen(); }; diff --git a/include/sigil.hpp b/include/sigil.hpp index 9a71146..7caf691 100644 --- a/include/sigil.hpp +++ b/include/sigil.hpp @@ -18,6 +18,9 @@ struct SigilVertex { struct GlyphRect { float u0, v0, u1, v1; + // Pixel-space metrics for positioning within a cell + int32_t bearing_x, bearing_y; + uint32_t width, height, advance; }; // Wayland type aliases to avoid namespace collisions @@ -129,6 +132,7 @@ namespace Kaelum { XdgToplevel* xdg_toplevel_ = nullptr; struct wl_seat* seat_ = nullptr; struct wl_keyboard* keyboard_ = nullptr; + struct wl_pointer* pointer_ = nullptr; // Vulkan handles VkInstance instance_ = VK_NULL_HANDLE; @@ -168,6 +172,7 @@ namespace Kaelum { VkBuffer vertex_buffer_ = VK_NULL_HANDLE; VkDeviceMemory vertex_buffer_memory_ = VK_NULL_HANDLE; void* vertex_buffer_mapped_ = nullptr; + size_t vertex_buffer_capacity_ = 0; // Glyph Atlas VkImage glyph_atlas_image_ = VK_NULL_HANDLE; diff --git a/src/glyph_engine.cpp b/src/glyph_engine.cpp index ce728c0..1cd816a 100644 --- a/src/glyph_engine.cpp +++ b/src/glyph_engine.cpp @@ -73,8 +73,8 @@ std::expected GlyphEngine::get_glyph(char32_t codepoint) FT_GlyphSlot slot = face_->glyph; GlyphData data; data.metric = { - .bearing_x = static_cast(slot->bitmap_left), - .bearing_y = static_cast(slot->bitmap_top), + .bearing_x = static_cast(slot->bitmap_left), + .bearing_y = static_cast(slot->bitmap_top), .width = static_cast(slot->bitmap.width), .height = static_cast(slot->bitmap.rows), .advance = static_cast(slot->advance.x >> 6) diff --git a/src/loom.cpp b/src/loom.cpp index 6726e42..dd5ffe2 100644 --- a/src/loom.cpp +++ b/src/loom.cpp @@ -113,10 +113,14 @@ std::expected Loom::poll_read(std::span buffer) { } std::expected Loom::write(std::span data) { + if (data.empty()) return {}; + size_t to_write = std::min(data.size(), k_write_buffer_size); + std::memcpy(write_buffer_, data.data(), to_write); + struct io_uring_sqe *sqe = io_uring_get_sqe(&ring_); if (!sqe) return std::unexpected(LoomError::WriteFailed); - io_uring_prep_write(sqe, master_fd_, data.data(), data.size(), -1); + io_uring_prep_write(sqe, master_fd_, write_buffer_, to_write, -1); io_uring_sqe_set_data64(sqe, URING_TAG_WRITE); io_uring_submit(&ring_); diff --git a/src/nexus.cpp b/src/nexus.cpp index edcbcb3..e323fdb 100644 --- a/src/nexus.cpp +++ b/src/nexus.cpp @@ -20,7 +20,10 @@ void Nexus::handle_ground(uint8_t c) { current_state_ = State::Escape; } else if (c == '\n') { cursor_y_++; - if (cursor_y_ >= rows_) cursor_y_ = rows_ - 1; + if (cursor_y_ >= rows_) { + scroll_up(); + cursor_y_ = rows_ - 1; + } cursor_x_ = 0; } else if (c == '\r') { cursor_x_ = 0; @@ -36,7 +39,10 @@ void Nexus::handle_ground(uint8_t c) { if (cursor_x_ >= cols_) { cursor_x_ = 0; cursor_y_++; - if (cursor_y_ >= rows_) cursor_y_ = rows_ - 1; + if (cursor_y_ >= rows_) { + scroll_up(); + cursor_y_ = rows_ - 1; + } } } } @@ -222,8 +228,15 @@ void Nexus::resize(size_t cols, size_t rows) { } void Nexus::move_cursor(int dx, int dy) { - cursor_x_ = std::clamp(static_cast(cursor_x_ + dx), size_t(0), cols_ - 1); - cursor_y_ = std::clamp(static_cast(cursor_y_ + dy), size_t(0), rows_ - 1); + int new_x = static_cast(cursor_x_) + dx; + int new_y = static_cast(cursor_y_) + dy; + cursor_x_ = static_cast(std::clamp(new_x, 0, static_cast(cols_) - 1)); + cursor_y_ = static_cast(std::clamp(new_y, 0, static_cast(rows_) - 1)); +} + +void Nexus::scroll_up() { + std::copy(grid_.begin() + cols_, grid_.end(), grid_.begin()); + std::fill(grid_.end() - cols_, grid_.end(), Cell{}); } void Nexus::set_cell(char32_t cp, Color fg, Color bg, uint32_t attrs) { diff --git a/src/sigil.cpp b/src/sigil.cpp index a433d86..ecfc391 100644 --- a/src/sigil.cpp +++ b/src/sigil.cpp @@ -55,6 +55,7 @@ Sigil::~Sigil() { // Wayland cleanup if (frame_callback_) wl_callback_destroy(frame_callback_); + if (pointer_) wl_pointer_destroy(pointer_); if (keyboard_) wl_keyboard_destroy(keyboard_); if (xdg_toplevel_) xdg_toplevel_destroy(xdg_toplevel_); if (xdg_surface_) xdg_surface_destroy(xdg_surface_); @@ -111,7 +112,7 @@ void registry_handle_global(void* data, struct wl_registry* registry, uint32_t i xdg_wm_base_add_listener(sigil->xdg_wm_base_, &wm_base_listener, sigil); std::println("Sigil: Bound xdg_wm_base"); } else if (iface == "wl_seat") { - sigil->seat_ = (struct wl_seat*)wl_registry_bind(registry, id, &wl_seat_interface, version); + sigil->seat_ = (struct wl_seat*)wl_registry_bind(registry, id, &wl_seat_interface, std::min(version, 5u)); std::println("Sigil: Bound wl_seat"); } } @@ -161,6 +162,18 @@ static const struct xdg_toplevel_listener xdg_toplevel_listener = { .wm_capabilities = [](void*, struct xdg_toplevel*, struct wl_array*) {}, }; +// Pointer listener — handles cursor enter/leave for proper Hyprland focus +static const struct wl_pointer_listener pointer_listener_inst = { + .enter = [](void*, struct wl_pointer* pointer, uint32_t serial, + struct wl_surface*, wl_fixed_t, wl_fixed_t) { + wl_pointer_set_cursor(pointer, serial, nullptr, 0, 0); + }, + .leave = [](void*, struct wl_pointer*, uint32_t, struct wl_surface*) {}, + .motion = [](void*, struct wl_pointer*, uint32_t, wl_fixed_t, wl_fixed_t) {}, + .button = [](void*, struct wl_pointer*, uint32_t, uint32_t, uint32_t, uint32_t) {}, + .axis = [](void*, struct wl_pointer*, uint32_t, uint32_t, wl_fixed_t) {}, +}; + static void keyboard_handle_key(void* data, struct wl_keyboard* keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { (void)keyboard; (void)serial; @@ -210,6 +223,11 @@ std::expected Sigil::init_wayland() { if (seat_) { keyboard_ = wl_seat_get_keyboard(seat_); wl_keyboard_add_listener(keyboard_, &keyboard_listener, this); + + pointer_ = wl_seat_get_pointer(seat_); + if (pointer_) { + wl_pointer_add_listener(pointer_, &pointer_listener_inst, this); + } std::println("Sigil: Keyboard listener attached."); } @@ -763,10 +781,11 @@ void Sigil::render(const Nexus& nexus) { size_t n_rows = nexus.rows(); std::vector vertices; - float cell_w = 2.0f / n_cols; - float cell_h = 2.0f / n_rows; - float origin_x = -1.0f; - float origin_y = 1.0f; + // Convert pixel dimensions to NDC units + float px_to_ndc_x = 2.0f / extent_.width; + float px_to_ndc_y = 2.0f / extent_.height; + float cell_w_ndc = cell_width_ * px_to_ndc_x; + float cell_h_ndc = cell_height_ * px_to_ndc_y; for (uint32_t y = 0; y < n_rows; ++y) { for (uint32_t x = 0; x < n_cols; ++x) { @@ -774,31 +793,36 @@ void Sigil::render(const Nexus& nexus) { if (cell.codepoint == U' ') continue; auto it = glyph_map_.find(cell.codepoint); - float u0 = 0, v0 = 0, u1 = 1, v1 = 1; - if (it != glyph_map_.end()) { - u0 = it->second.u0; - v0 = it->second.v0; - u1 = it->second.u1; - v1 = it->second.v1; - } + if (it == glyph_map_.end()) continue; + + const auto& gr = it->second; + + // Cell origin in NDC (top-left of cell) + float cell_x = -1.0f + x * cell_w_ndc; + float cell_y = 1.0f - y * cell_h_ndc; - float x0 = origin_x + x * cell_w; - float y0 = origin_y - (y + 1) * cell_h; - float x1 = x0 + cell_w; - float y1 = origin_y - y * cell_h; + // Position glyph within cell using bearing offsets + float glyph_x0 = cell_x + gr.bearing_x * px_to_ndc_x; + float glyph_y0 = cell_y - gr.bearing_y * px_to_ndc_y; + float glyph_x1 = glyph_x0 + gr.width * px_to_ndc_x; + float glyph_y1 = glyph_y0 + gr.height * px_to_ndc_y; float r = cell.fg.r / 255.0f, g = cell.fg.g / 255.0f; float b = cell.fg.b / 255.0f, a = cell.fg.a / 255.0f; - vertices.push_back({{x0, y0}, {u0, v0}, {r, g, b, a}}); - vertices.push_back({{x1, y0}, {u1, v0}, {r, g, b, a}}); - vertices.push_back({{x0, y1}, {u0, v1}, {r, g, b, a}}); - vertices.push_back({{x1, y0}, {u1, v0}, {r, g, b, a}}); - vertices.push_back({{x1, y1}, {u1, v1}, {r, g, b, a}}); - vertices.push_back({{x0, y1}, {u0, v1}, {r, g, b, a}}); + // Two triangles for the glyph quad (top-left origin, Y-down in NDC) + vertices.push_back({{glyph_x0, glyph_y1}, {gr.u0, gr.v1}, {r, g, b, a}}); + vertices.push_back({{glyph_x1, glyph_y1}, {gr.u1, gr.v1}, {r, g, b, a}}); + vertices.push_back({{glyph_x0, glyph_y0}, {gr.u0, gr.v0}, {r, g, b, a}}); + vertices.push_back({{glyph_x1, glyph_y1}, {gr.u1, gr.v1}, {r, g, b, a}}); + vertices.push_back({{glyph_x1, glyph_y0}, {gr.u1, gr.v0}, {r, g, b, a}}); + vertices.push_back({{glyph_x0, glyph_y0}, {gr.u0, gr.v0}, {r, g, b, a}}); } } + if (vertices.size() > vertex_buffer_capacity_) { + vertices.resize(vertex_buffer_capacity_); + } if (!vertices.empty()) { std::memcpy(vertex_buffer_mapped_, vertices.data(), vertices.size() * sizeof(SigilVertex)); } @@ -890,14 +914,14 @@ void Sigil::render(const Nexus& nexus) { void Sigil::handle_configure(uint32_t width, uint32_t height) { if (width > 0 && height > 0) { - configured_width_ = width; - configured_height_ = height; - } - if (initial_configure_done_) { - needs_resize_ = true; + if (width != configured_width_ || height != configured_height_) { + configured_width_ = width; + configured_height_ = height; + if (initial_configure_done_) { + needs_resize_ = true; + } + } } - std::println("Sigil: Configure {}x{} (stored: {}x{}, init_done: {})", - width, height, configured_width_, configured_height_, initial_configure_done_); } void Sigil::process_pending_resize() { @@ -1095,7 +1119,12 @@ void Sigil::create_glyph_atlas(GlyphEngine& engine) { (float)current_x / atlas_width, (float)current_y / atlas_height, (float)(current_x + glyph.metric.width) / atlas_width, - (float)(current_y + glyph.metric.height) / atlas_height + (float)(current_y + glyph.metric.height) / atlas_height, + static_cast(glyph.metric.bearing_x), + static_cast(glyph.metric.bearing_y), + glyph.metric.width, + glyph.metric.height, + glyph.metric.advance }; current_x += glyph.metric.width; @@ -1227,7 +1256,8 @@ uint32_t Sigil::find_memory_type(uint32_t typeFilter, VkMemoryPropertyFlags prop void Sigil::create_vertex_buffer() { VkBufferCreateInfo buffer_info{}; buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; - buffer_info.size = 10000 * 6 * sizeof(SigilVertex); // Max estimate for grid + vertex_buffer_capacity_ = 20000 * 6; + buffer_info.size = vertex_buffer_capacity_ * sizeof(SigilVertex); buffer_info.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; From ea859834461dab1f18b31dadc2fdb32b40311966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 10:57:54 +0000 Subject: [PATCH 12/17] fix: add missing wl_pointer frame/axis_source/axis_stop/axis_discrete handlers wl_pointer version 5 requires all event handlers up to axis_discrete. Missing frame handler (opcode 5) caused SIGABRT on Hyprland. --- src/sigil.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sigil.cpp b/src/sigil.cpp index ecfc391..03f6def 100644 --- a/src/sigil.cpp +++ b/src/sigil.cpp @@ -172,6 +172,10 @@ static const struct wl_pointer_listener pointer_listener_inst = { .motion = [](void*, struct wl_pointer*, uint32_t, wl_fixed_t, wl_fixed_t) {}, .button = [](void*, struct wl_pointer*, uint32_t, uint32_t, uint32_t, uint32_t) {}, .axis = [](void*, struct wl_pointer*, uint32_t, uint32_t, wl_fixed_t) {}, + .frame = [](void*, struct wl_pointer*) {}, + .axis_source = [](void*, struct wl_pointer*, uint32_t) {}, + .axis_stop = [](void*, struct wl_pointer*, uint32_t, uint32_t) {}, + .axis_discrete = [](void*, struct wl_pointer*, uint32_t, int32_t) {}, }; static void keyboard_handle_key(void* data, struct wl_keyboard* keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { From 526bcfc67fdcf049ff82e2bb178e738353a61dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 11:03:28 +0000 Subject: [PATCH 13/17] fix: correct Vulkan NDC Y-axis, pointer listener completeness, vertex buffer map size Rendering: - Fixed Y-axis direction: Vulkan NDC has Y=-1 at top, Y=+1 at bottom. Row 0 now renders at top of screen instead of bottom. - Baseline positioned at bottom of each cell; bearing_y offsets glyph upward from baseline (correct for Y-down coordinate system). - Triangle winding order corrected for Y-down NDC. - Added codepoint==0 skip alongside space skip. - Fixed vertex buffer map size (was hardcoded 10k, now uses capacity). Pointer: - Added axis_value120 and axis_relative_direction handlers to complete the wl_pointer v5+ listener (fixes missing field warning). --- src/sigil.cpp | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/sigil.cpp b/src/sigil.cpp index 03f6def..12903b5 100644 --- a/src/sigil.cpp +++ b/src/sigil.cpp @@ -176,6 +176,8 @@ static const struct wl_pointer_listener pointer_listener_inst = { .axis_source = [](void*, struct wl_pointer*, uint32_t) {}, .axis_stop = [](void*, struct wl_pointer*, uint32_t, uint32_t) {}, .axis_discrete = [](void*, struct wl_pointer*, uint32_t, int32_t) {}, + .axis_value120 = [](void*, struct wl_pointer*, uint32_t, int32_t) {}, + .axis_relative_direction = [](void*, struct wl_pointer*, uint32_t, uint32_t) {}, }; static void keyboard_handle_key(void* data, struct wl_keyboard* keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { @@ -786,6 +788,7 @@ void Sigil::render(const Nexus& nexus) { std::vector vertices; // Convert pixel dimensions to NDC units + // Vulkan NDC: X [-1,+1] left→right, Y [-1,+1] top→bottom float px_to_ndc_x = 2.0f / extent_.width; float px_to_ndc_y = 2.0f / extent_.height; float cell_w_ndc = cell_width_ * px_to_ndc_x; @@ -794,41 +797,46 @@ void Sigil::render(const Nexus& nexus) { for (uint32_t y = 0; y < n_rows; ++y) { for (uint32_t x = 0; x < n_cols; ++x) { const auto& cell = grid[y * n_cols + x]; - if (cell.codepoint == U' ') continue; + if (cell.codepoint == U' ' || cell.codepoint == 0) continue; auto it = glyph_map_.find(cell.codepoint); if (it == glyph_map_.end()) continue; const auto& gr = it->second; - // Cell origin in NDC (top-left of cell) - float cell_x = -1.0f + x * cell_w_ndc; - float cell_y = 1.0f - y * cell_h_ndc; + // Cell top-left in NDC (Y-down: row 0 at top = Y=-1) + float cell_left = -1.0f + x * cell_w_ndc; + float cell_top = -1.0f + y * cell_h_ndc; - // Position glyph within cell using bearing offsets - float glyph_x0 = cell_x + gr.bearing_x * px_to_ndc_x; - float glyph_y0 = cell_y - gr.bearing_y * px_to_ndc_y; + // Baseline is near the bottom of the cell. + // bearing_y = distance from baseline to glyph top (positive = up) + // In Y-down NDC, "up" means subtracting from baseline Y + float baseline_y = cell_top + cell_h_ndc; // bottom of cell = baseline + float glyph_x0 = cell_left + gr.bearing_x * px_to_ndc_x; + float glyph_y0 = baseline_y - gr.bearing_y * px_to_ndc_y; // top of glyph float glyph_x1 = glyph_x0 + gr.width * px_to_ndc_x; - float glyph_y1 = glyph_y0 + gr.height * px_to_ndc_y; + float glyph_y1 = glyph_y0 + gr.height * px_to_ndc_y; // bottom of glyph float r = cell.fg.r / 255.0f, g = cell.fg.g / 255.0f; float b = cell.fg.b / 255.0f, a = cell.fg.a / 255.0f; - // Two triangles for the glyph quad (top-left origin, Y-down in NDC) - vertices.push_back({{glyph_x0, glyph_y1}, {gr.u0, gr.v1}, {r, g, b, a}}); - vertices.push_back({{glyph_x1, glyph_y1}, {gr.u1, gr.v1}, {r, g, b, a}}); + // Two triangles: top-left, top-right, bottom-left; top-right, bottom-right, bottom-left + // UV: v0=top of glyph bitmap, v1=bottom vertices.push_back({{glyph_x0, glyph_y0}, {gr.u0, gr.v0}, {r, g, b, a}}); - vertices.push_back({{glyph_x1, glyph_y1}, {gr.u1, gr.v1}, {r, g, b, a}}); vertices.push_back({{glyph_x1, glyph_y0}, {gr.u1, gr.v0}, {r, g, b, a}}); - vertices.push_back({{glyph_x0, glyph_y0}, {gr.u0, gr.v0}, {r, g, b, a}}); + vertices.push_back({{glyph_x0, glyph_y1}, {gr.u0, gr.v1}, {r, g, b, a}}); + vertices.push_back({{glyph_x1, glyph_y0}, {gr.u1, gr.v0}, {r, g, b, a}}); + vertices.push_back({{glyph_x1, glyph_y1}, {gr.u1, gr.v1}, {r, g, b, a}}); + vertices.push_back({{glyph_x0, glyph_y1}, {gr.u0, gr.v1}, {r, g, b, a}}); } } if (vertices.size() > vertex_buffer_capacity_) { vertices.resize(vertex_buffer_capacity_); } + size_t vertex_count = vertices.size(); if (!vertices.empty()) { - std::memcpy(vertex_buffer_mapped_, vertices.data(), vertices.size() * sizeof(SigilVertex)); + std::memcpy(vertex_buffer_mapped_, vertices.data(), vertex_count * sizeof(SigilVertex)); } // --- Submit Render Command --- @@ -879,7 +887,7 @@ void Sigil::render(const Nexus& nexus) { vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout_, 0, 1, &descriptor_set_, 0, nullptr); VkDeviceSize offsets[] = {0}; vkCmdBindVertexBuffers(cb, 0, 1, &vertex_buffer_, offsets); - vkCmdDraw(cb, static_cast(vertices.size()), 1, 0, 0); + vkCmdDraw(cb, static_cast(vertex_count), 1, 0, 0); vkCmdEndRenderPass(cb); vkEndCommandBuffer(cb); @@ -1285,7 +1293,7 @@ void Sigil::create_vertex_buffer() { } vkBindBufferMemory(device_, vertex_buffer_, vertex_buffer_memory_, 0); - vkMapMemory(device_, vertex_buffer_memory_, 0, 10000 * 6 * sizeof(SigilVertex), 0, &vertex_buffer_mapped_); + vkMapMemory(device_, vertex_buffer_memory_, 0, vertex_buffer_capacity_ * sizeof(SigilVertex), 0, &vertex_buffer_mapped_); } void Sigil::create_descriptor_set() { From a55457d6acc2c12a18c02ee41a3aa16092223027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 11:07:32 +0000 Subject: [PATCH 14/17] fix: initial grid resize to match compositor, add render diagnostics - Grid and PTY size now synced to actual compositor dimensions at startup (was stuck at 80x24 until first resize event fired in main loop) - Added temporary render diagnostics: first 5 frames print grid dimensions, non-space cell count, vertex count, and swapchain extent - Fixed vertex buffer map size to use capacity variable --- include/sigil.hpp | 1 + src/main.cpp | 14 ++++++++++++-- src/sigil.cpp | 10 ++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/include/sigil.hpp b/include/sigil.hpp index 7caf691..f991f5c 100644 --- a/include/sigil.hpp +++ b/include/sigil.hpp @@ -148,6 +148,7 @@ namespace Kaelum { bool needs_resize_ = false; bool should_close_ = false; bool frame_pending_ = false; + uint32_t render_diag_count_ = 0; struct wl_callback* frame_callback_ = nullptr; std::vector swapchain_images_; std::vector swapchain_image_views_; diff --git a/src/main.cpp b/src/main.cpp index aca152e..b4c6e7c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -47,8 +47,18 @@ int main() { static_cast(wpx), static_cast(hpx)); }); - // Set initial PTY size - loom.set_pty_size(static_cast(Kaelum::k_default_cols), static_cast(Kaelum::k_default_rows)); + // Sync grid + PTY to the actual window size from the compositor + { + uint32_t wpx = sigil.configured_width(); + uint32_t hpx = sigil.configured_height(); + uint32_t cols = (cw > 0) ? wpx / cw : Kaelum::k_default_cols; + uint32_t rows = (ch > 0) ? hpx / ch : Kaelum::k_default_rows; + if (cols == 0) cols = 1; + if (rows == 0) rows = 1; + nexus.resize(cols, rows); + loom.set_pty_size(static_cast(cols), static_cast(rows), + static_cast(wpx), static_cast(hpx)); + } // Map Linux input scancodes to ASCII std::map key_map = { diff --git a/src/sigil.cpp b/src/sigil.cpp index 12903b5..bfe2360 100644 --- a/src/sigil.cpp +++ b/src/sigil.cpp @@ -835,6 +835,16 @@ void Sigil::render(const Nexus& nexus) { vertices.resize(vertex_buffer_capacity_); } size_t vertex_count = vertices.size(); + if (render_diag_count_ < 5) { + size_t non_space = 0; + for (size_t i = 0; i < n_rows * n_cols; ++i) { + if (grid[i].codepoint != U' ' && grid[i].codepoint != 0) ++non_space; + } + std::println("Sigil: render#{} grid={}x{} non_space={} verts={} extent={}x{}", + render_diag_count_, n_cols, n_rows, non_space, vertex_count, + extent_.width, extent_.height); + ++render_diag_count_; + } if (!vertices.empty()) { std::memcpy(vertex_buffer_mapped_, vertices.data(), vertex_count * sizeof(SigilVertex)); } From aca6906466ced0b5b2e7459d2e03dad75cddf3fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 11:09:07 +0000 Subject: [PATCH 15/17] =?UTF-8?q?fix:=20remove=20O=5FNONBLOCK=20from=20PTY?= =?UTF-8?q?=20master=20fd=20=E2=80=94=20breaks=20io=5Furing=20reads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit io_uring reads from non-blocking FDs complete immediately with -EAGAIN when no data is available, instead of waiting for the shell to produce output. This caused all io_uring read completions to fail silently, so shell output never reached the grid (confirmed by render diagnostics showing non_space=0 across all frames). With the FD in blocking mode, io_uring correctly parks the read in the kernel until the shell writes data, then signals the registered eventfd. --- src/loom.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/loom.cpp b/src/loom.cpp index dd5ffe2..ba884b7 100644 --- a/src/loom.cpp +++ b/src/loom.cpp @@ -54,11 +54,9 @@ std::expected Loom::initialize() { } initialized_ = true; - // Set master_fd to non-blocking for safety - - int flags = fcntl(master_fd_, F_GETFL, 0); - fcntl(master_fd_, F_SETFL, flags | O_NONBLOCK); - + // io_uring handles blocking FDs natively — the kernel completes the + // read asynchronously when data arrives. O_NONBLOCK would cause + // immediate -EAGAIN completions instead of waiting for shell output. submit_read(); return {}; } From 99f82227e98f8052119e0a7eb9cc21ccb74c7b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 11:13:41 +0000 Subject: [PATCH 16/17] =?UTF-8?q?fix:=20escape=20sequence=20parser=20?= =?UTF-8?q?=E2=80=94=20handle=20DEC=20private=20modes,=20OSC,=20charset=20?= =?UTF-8?q?designations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fish shell prompt was invisible because most of its output consists of escape sequences that the parser was mishandling: - CSI '?' prefix (DEC private modes like ?2004h bracketed paste, ?25h show cursor) was treated as the final byte, causing '2004h' to render as text - ESC ( ) * + charset designations (3-byte sequences) were partially consumed, leaking the final byte as visible text - OSC sequences only terminated on BEL; ESC \ (ST) wasn't handled, leaving the parser stuck in OSC state eating all subsequent output Also: - Stop hiding mouse cursor on pointer enter (was setting cursor to NULL) - Implement CSI f (HVP), X (ECH), S (SU), r/c/n/l/h/s/u stubs - Add EscapeSkip state for consuming charset designation final byte --- include/nexus.hpp | 4 ++- src/nexus.cpp | 72 ++++++++++++++++++++++++++++++++++++++++++----- src/sigil.cpp | 4 +-- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/include/nexus.hpp b/include/nexus.hpp index 283a958..5b41d4b 100644 --- a/include/nexus.hpp +++ b/include/nexus.hpp @@ -17,7 +17,7 @@ namespace Kaelum { Ground = 0, Escape, CSI, - SGR, + EscapeSkip, // Consume one byte after ESC ( ) * + OSC, Count // Sentinel for dispatch table size }; @@ -48,6 +48,7 @@ namespace Kaelum { void handle_ground(uint8_t c); void handle_escape(uint8_t c); void handle_csi(uint8_t c); + void handle_escape_skip(uint8_t c); void handle_osc(uint8_t c); // Sequence helpers @@ -61,6 +62,7 @@ namespace Kaelum { size_t cursor_y_ = 0; State current_state_ = State::Ground; std::vector sequence_buffer_; + char csi_prefix_ = 0; // '?' for DEC private, '>' for secondary DA, 0 for standard void move_cursor(int dx, int dy); void scroll_up(); diff --git a/src/nexus.cpp b/src/nexus.cpp index e323fdb..6985d7c 100644 --- a/src/nexus.cpp +++ b/src/nexus.cpp @@ -12,6 +12,7 @@ Nexus::Nexus() { dispatch_table_[static_cast(State::Ground)] = &Nexus::handle_ground; dispatch_table_[static_cast(State::Escape)] = &Nexus::handle_escape; dispatch_table_[static_cast(State::CSI)] = &Nexus::handle_csi; + dispatch_table_[static_cast(State::EscapeSkip)] = &Nexus::handle_escape_skip; dispatch_table_[static_cast(State::OSC)] = &Nexus::handle_osc; } @@ -51,28 +52,41 @@ void Nexus::handle_escape(uint8_t c) { if (c == '[') { current_state_ = State::CSI; sequence_buffer_.clear(); + csi_prefix_ = 0; } else if (c == ']') { current_state_ = State::OSC; sequence_buffer_.clear(); + } else if (c == '(' || c == ')' || c == '*' || c == '+') { + // Character set designation — next byte selects the set; skip it + current_state_ = State::EscapeSkip; } else { - // Not a valid sequence, treat as normal text (ESC + char) - handle_ground(27); - handle_ground(c); + // Single-character escape: M (reverse index), 7/8 (save/restore cursor), + // =, >, c, etc. — silently ignore for now current_state_ = State::Ground; } } +void Nexus::handle_escape_skip(uint8_t /*c*/) { + current_state_ = State::Ground; +} + void Nexus::handle_csi(uint8_t c) { - if (std::isdigit(c) || c == ';') { + if (c == '?' || c == '>' || c == '!') { + csi_prefix_ = static_cast(c); + } else if (std::isdigit(c) || c == ';') { sequence_buffer_.push_back(c); } else { process_csi(c); sequence_buffer_.clear(); + csi_prefix_ = 0; current_state_ = State::Ground; } } void Nexus::process_csi(uint8_t final_char) { + // DEC private mode sequences (CSI ? ...) and secondary DA (CSI > ...) — silently ignore + if (csi_prefix_ != 0) return; + // Parse parameters from sequence_buffer_ std::vector params; int current_param = 0; @@ -111,8 +125,13 @@ void Nexus::process_csi(uint8_t final_char) { cursor_y_ = std::clamp(row - 1, 0, (int)rows_ - 1); } break; - case 'f': // Cursor Position (CUP) - // Same as 'H' + case 'f': // Cursor Position (HVP) — same as 'H' + { + int row = params.size() > 0 ? params[0] : 1; + int col = params.size() > 1 ? params[1] : 1; + cursor_x_ = std::clamp(col - 1, 0, (int)cols_ - 1); + cursor_y_ = std::clamp(row - 1, 0, (int)rows_ - 1); + } break; case 'J': // Erase in Display (ED) { @@ -202,6 +221,35 @@ void Nexus::process_csi(uint8_t final_char) { case 'm': // Select Graphic Rendition (SGR) parse_sgr(sequence_buffer_); break; + case 'r': // Set Scrolling Region (DECSTBM) — ignored for now + break; + case 'c': // Device Attributes (DA) — ignored, query from shell + break; + case 'n': // Device Status Report — ignored + break; + case 'l': // Reset Mode — standard modes, ignored + break; + case 'h': // Set Mode — standard modes, ignored + break; + case 's': // Save Cursor Position (SCP) + break; + case 'u': // Restore Cursor Position (RCP) + break; + case 'X': // Erase Characters (ECH) + { + int n = params.empty() ? 1 : params[0]; + for (int i = 0; i < n && cursor_x_ + i < cols_; ++i) + grid_[cursor_y_ * cols_ + cursor_x_ + i] = Cell{}; + } + break; + case 'S': // Scroll Up + { + int n = params.empty() ? 1 : params[0]; + for (int i = 0; i < n; ++i) scroll_up(); + } + break; + case 'T': // Scroll Down + break; default: break; } @@ -250,7 +298,17 @@ void Nexus::clear_screen() { } void Nexus::handle_osc(uint8_t c) { - if (c == '\a' || c == '\n') { + if (c == '\a') { + // BEL terminates OSC + sequence_buffer_.clear(); + current_state_ = State::Ground; + } else if (c == 27) { + // ESC inside OSC — likely ST (\e\\). Consume next byte if it's '\\'. + // For simplicity, just end the OSC now. + sequence_buffer_.clear(); + current_state_ = State::Ground; + } else if (c == 0x9c) { + // ST (C1 control) sequence_buffer_.clear(); current_state_ = State::Ground; } else { diff --git a/src/sigil.cpp b/src/sigil.cpp index bfe2360..d3409d5 100644 --- a/src/sigil.cpp +++ b/src/sigil.cpp @@ -164,9 +164,9 @@ static const struct xdg_toplevel_listener xdg_toplevel_listener = { // Pointer listener — handles cursor enter/leave for proper Hyprland focus static const struct wl_pointer_listener pointer_listener_inst = { - .enter = [](void*, struct wl_pointer* pointer, uint32_t serial, + .enter = [](void*, struct wl_pointer*, uint32_t, struct wl_surface*, wl_fixed_t, wl_fixed_t) { - wl_pointer_set_cursor(pointer, serial, nullptr, 0, 0); + // Don't call wl_pointer_set_cursor — let the compositor keep its default cursor }, .leave = [](void*, struct wl_pointer*, uint32_t, struct wl_surface*) {}, .motion = [](void*, struct wl_pointer*, uint32_t, wl_fixed_t, wl_fixed_t) {}, From 4f5eff904564061a521ea02e792b49ea290ad0ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=D0=B0=D0=B5=20=D0=95u=D0=BD=D1=88=D0=B0?= Date: Fri, 19 Jun 2026 11:16:22 +0000 Subject: [PATCH 17/17] fix: replace io_uring PTY reads with direct poll()+read() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit io_uring reads from PTY file descriptors were silently failing — the eventfd was never signaled, so shell output never reached the grid (non_space=0 across all frames). Replaced with standard poll(master_fd)+read() pattern, which is what Ghostty, Kitty, and every production terminal emulator uses. io_uring is kept for async writes only. Also increased render diagnostic count to 10 frames for better debugging. --- src/loom.cpp | 10 ++++++---- src/main.cpp | 41 ++++++++++++++++++++--------------------- src/sigil.cpp | 2 +- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/loom.cpp b/src/loom.cpp index ba884b7..62bdd03 100644 --- a/src/loom.cpp +++ b/src/loom.cpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace { constexpr uint64_t URING_TAG_READ = 1; @@ -54,10 +55,11 @@ std::expected Loom::initialize() { } initialized_ = true; - // io_uring handles blocking FDs natively — the kernel completes the - // read asynchronously when data arrives. O_NONBLOCK would cause - // immediate -EAGAIN completions instead of waiting for shell output. - submit_read(); + // Set non-blocking for poll()+read() pattern in main loop. + // PTY reads now happen via direct read() instead of io_uring. + int flags = fcntl(master_fd_, F_GETFL, 0); + fcntl(master_fd_, F_SETFL, flags | O_NONBLOCK); + return {}; } diff --git a/src/main.cpp b/src/main.cpp index b4c6e7c..6b70063 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "loom.hpp" #include "nexus.hpp" #include "sigil.hpp" @@ -106,18 +107,13 @@ int main() { std::vector buffer(4096); - // Event-driven setup int wayland_fd = sigil.get_display_fd(); - auto wake_fd_res = loom.register_wake_fd(); - if (!wake_fd_res) { - std::println(stderr, "Failed to register io_uring wake FD."); - return 1; - } - int wake_fd = *wake_fd_res; + int pty_fd = loom.master_fd(); + // Poll on Wayland display FD + PTY master FD directly std::vector poll_fds = { { wayland_fd, POLLIN, 0 }, - { wake_fd, POLLIN, 0 } + { pty_fd, POLLIN, 0 } }; while (!sigil.should_close()) { @@ -131,9 +127,13 @@ int main() { continue; } - int ret = poll(poll_fds.data(), poll_fds.size(), 1); + int ret = poll(poll_fds.data(), poll_fds.size(), 16); if (ret < 0) { + if (errno == EINTR) { + sigil.cancel_read(); + continue; + } sigil.cancel_read(); std::println(stderr, "Poll error."); break; @@ -145,21 +145,20 @@ int main() { sigil.cancel_read(); } + // Read PTY output directly — standard read() + poll is what + // Ghostty, Kitty, and every production terminal uses if (poll_fds[1].revents & POLLIN) { - // Consume the eventfd to prevent busy-loop - uint64_t wake_val; - [[maybe_unused]] auto r = ::read(wake_fd, &wake_val, sizeof(wake_val)); - - auto read_res = loom.poll_read(buffer); - if (read_res) { - size_t bytes = *read_res; - if (bytes > 0) { - auto process_res = nexus.process_input({buffer.data(), bytes}); - if (!process_res) { - std::println(stderr, "Nexus parsing error."); - } + ssize_t n = ::read(pty_fd, buffer.data(), buffer.size()); + if (n > 0) { + auto process_res = nexus.process_input({buffer.data(), static_cast(n)}); + if (!process_res) { + std::println(stderr, "Nexus parsing error."); } + } else if (n == 0) { + // Shell exited + break; } + // n < 0 && errno == EAGAIN: no data, continue } sigil.process_pending_resize(); diff --git a/src/sigil.cpp b/src/sigil.cpp index d3409d5..f1bf6c4 100644 --- a/src/sigil.cpp +++ b/src/sigil.cpp @@ -835,7 +835,7 @@ void Sigil::render(const Nexus& nexus) { vertices.resize(vertex_buffer_capacity_); } size_t vertex_count = vertices.size(); - if (render_diag_count_ < 5) { + if (render_diag_count_ < 10) { size_t non_space = 0; for (size_t i = 0; i < n_rows * n_cols; ++i) { if (grid[i].codepoint != U' ' && grid[i].codepoint != 0) ++non_space;