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..88b8a4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,11 +45,11 @@ 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") -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) @@ -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..77cf216 100644 --- a/include/common.hpp +++ b/include/common.hpp @@ -2,7 +2,7 @@ #include #include -#include +#include namespace Kaelum { @@ -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; @@ -39,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..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; @@ -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 2967feb..853ab39 100644 --- a/include/loom.hpp +++ b/include/loom.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "common.hpp" @@ -56,16 +57,26 @@ 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(); + 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; + 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 b781c11..5b41d4b 100644 --- a/include/nexus.hpp +++ b/include/nexus.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include "common.hpp" namespace Kaelum { @@ -18,7 +17,7 @@ namespace Kaelum { Ground = 0, Escape, CSI, - SGR, + EscapeSkip, // Consume one byte after ESC ( ) * + OSC, Count // Sentinel for dispatch table size }; @@ -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: @@ -45,19 +48,24 @@ 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 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; 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(); 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 e7fd800..f991f5c 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 @@ -69,18 +72,36 @@ 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; } + + 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. */ 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. */ int get_display_fd() const { return wl_display_get_fd(display_); } + WaylandSurface* get_wl_surface() const { return wl_surface_; } /** @@ -88,6 +109,16 @@ 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); + + /** + * @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); @@ -101,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; @@ -110,12 +142,23 @@ 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; + 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_; std::vector swapchain_framebuffers_; 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; @@ -125,12 +168,12 @@ 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; 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; @@ -143,6 +186,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; @@ -151,7 +201,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/glyph_engine.cpp b/src/glyph_engine.cpp index 65ba2ed..1cd816a 100644 --- a/src/glyph_engine.cpp +++ b/src/glyph_engine.cpp @@ -1,5 +1,4 @@ #include "glyph_engine.hpp" -#include #include #include @@ -51,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)); @@ -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 f9eb879..62bdd03 100644 --- a/src/loom.cpp +++ b/src/loom.cpp @@ -2,10 +2,16 @@ #include #include #include -#include +#include #include #include #include +#include + +namespace { + constexpr uint64_t URING_TAG_READ = 1; + constexpr uint64_t URING_TAG_WRITE = 2; +} namespace Kaelum { @@ -34,9 +40,12 @@ std::expected Loom::initialize() { } if (child_pid_ == 0) { - // Child: Execute Fish Shell - execl("/usr/bin/fish", "fish", nullptr); - // If execl fails + // 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); + // If execl fails, try /bin/sh as last resort + execl("/bin/sh", "sh", nullptr); std::exit(1); } @@ -46,19 +55,20 @@ std::expected Loom::initialize() { } initialized_ = true; - // Set master_fd to non-blocking for safety - + // 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); - // Submit initial read request - struct io_uring_sqe *sqe = io_uring_get_sqe(&ring_); - if (!sqe) return std::unexpected(LoomError::ReadFailed); + 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() { @@ -75,53 +85,45 @@ 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) { + 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(), 0); + 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_); - - // 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 4c960a5..6b70063 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,10 +1,9 @@ -#include #include #include -#include -#include #include #include +#include +#include #include "loom.hpp" #include "nexus.hpp" #include "sigil.hpp" @@ -27,29 +26,76 @@ 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); - - // Map wayland keycodes to ASCII + // 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)); + }); + + // 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 = { - {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'}, + }; + + // 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](uint32_t key, bool pressed) { - if (pressed && key_map.contains(key)) { + sigil.set_keyboard_callback([&loom, &key_map, &special_key_map](uint32_t key, bool pressed) { + if (!pressed) return; + 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}); } @@ -61,54 +107,61 @@ 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 (true) { - // 1. Prepare Wayland for reading - sigil.prepare_read(); - - // Poll for events - int ret = poll(poll_fds.data(), poll_fds.size(), 1); - + while (!sigil.should_close()) { + sigil.dispatch_pending(); + sigil.flush(); + + if (!sigil.prepare_read()) { + sigil.dispatch_pending(); + sigil.process_pending_resize(); + sigil.render(nexus); + continue; + } + + 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; } 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.dispatch_pending(); + 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) { - 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(); sigil.render(nexus); } diff --git a/src/nexus.cpp b/src/nexus.cpp index 2cb70a3..6985d7c 100644 --- a/src/nexus.cpp +++ b/src/nexus.cpp @@ -1,17 +1,18 @@ #include "nexus.hpp" -#include #include +#include #include 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; 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; } @@ -19,17 +20,30 @@ 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_) { + scroll_up(); + 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), {255, 255, 255, 255}, {0, 0, 0, 255}, 0); - cursor_x_ = (cursor_x_ + 1) % k_default_cols; - if (cursor_x_ == 0) { - cursor_y_ = (cursor_y_ + 1) % k_default_rows; + set_cell(static_cast(c), vz_fg, vz_bg, 0); + cursor_x_++; + if (cursor_x_ >= cols_) { + cursor_x_ = 0; + cursor_y_++; + if (cursor_y_ >= rows_) { + scroll_up(); + cursor_y_ = rows_ - 1; + } } } } @@ -38,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; @@ -94,49 +121,194 @@ 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) - // 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) { 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; + 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; } } -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::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); + 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) { - 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; } 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 eac63d6..f1bf6c4 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,18 @@ 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 (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_); + 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 +73,23 @@ 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()); - + initial_configure_done_ = true; std::println("Sigil: Full hardware pipeline initialized successfully."); return {}; } @@ -86,9 +104,15 @@ 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); + sigil->seat_ = (struct wl_seat*)wl_registry_bind(registry, id, &wl_seat_interface, std::min(version, 5u)); std::println("Sigil: Bound wl_seat"); } } @@ -100,8 +124,17 @@ 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); + 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()); + } } static const struct xdg_surface_listener xdg_surface_listener = { @@ -109,22 +142,44 @@ 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->on_resize(static_cast(width), static_cast(height)); + 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->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*) {}, }; +// Pointer listener — handles cursor enter/leave for proper Hyprland focus +static const struct wl_pointer_listener pointer_listener_inst = { + .enter = [](void*, struct wl_pointer*, uint32_t, + struct wl_surface*, wl_fixed_t, wl_fixed_t) { + // 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) {}, + .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) {}, + .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) { (void)keyboard; (void)serial; @@ -143,29 +198,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); @@ -175,10 +224,16 @@ 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_); 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."); } @@ -200,9 +255,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 +290,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 +299,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 +339,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 {}; } @@ -298,16 +370,10 @@ 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); - - 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); - } - + uint32_t format_count; vkGetPhysicalDeviceSurfaceFormatsKHR(physical_device_, vk_surface_, &format_count, nullptr); if (format_count == 0) { @@ -334,6 +400,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; @@ -345,11 +412,18 @@ std::expected Sigil::create_swapchain() { } VkExtent2D actualExtent = capabilities.currentExtent; - if (actualExtent.width == UINT32_MAX) { - actualExtent = {800, 600}; + 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) { + return std::unexpected(SigilError::AllocationFailed); + } VkSwapchainCreateInfoKHR create_info{}; create_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; @@ -365,12 +439,27 @@ 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; + create_info.oldSwapchain = old_swapchain; - 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); } @@ -396,7 +485,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 +511,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 +567,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 +677,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 +685,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 +713,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); } @@ -673,96 +754,99 @@ 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); - 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; } - // --- Update Vertex Buffer from Nexus Grid --- + 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; - - // 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 { - 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 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; - - 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 - + // 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; + 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) { + const auto& cell = grid[y * n_cols + x]; + if (cell.codepoint == U' ' || cell.codepoint == 0) 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; - } - - 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}}); + if (it == glyph_map_.end()) continue; + + const auto& gr = it->second; + + // 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; + + // 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; // 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: 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_y0}, {gr.u1, 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 (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; + } + 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(), vertices.size() * sizeof(SigilVertex)); + std::memcpy(vertex_buffer_mapped_, vertices.data(), vertex_count * sizeof(SigilVertex)); } // --- Submit Render Command --- @@ -784,7 +868,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 +880,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; @@ -812,7 +897,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); @@ -835,31 +920,75 @@ 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) { + if (width > 0 && height > 0) { + if (width != configured_width_ || height != configured_height_) { + configured_width_ = width; + configured_height_ = height; + if (initial_configure_done_) { + 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) { + configured_width_ = width; + configured_height_ = height; + } vkDeviceWaitIdle(device_); cleanup_swapchain(); - 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(); - - // 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); + + 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() { 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"); @@ -867,6 +996,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); @@ -886,7 +1021,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 +1040,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 +1110,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; @@ -1010,19 +1141,21 @@ 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; 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 +1167,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 +1262,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) { @@ -1148,7 +1278,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; @@ -1172,7 +1303,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() { @@ -1195,6 +1326,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");