/* Crafter®.Graphics Copyright (C) 2026 Catcrafts® catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3.0 as published by the Free Software Foundation; This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM #include "vulkan/vulkan.h" #endif export module Crafter.Graphics:UI; import std; import Crafter.Event; import :Window; import :RenderPass; import :GraphicsTypes; import :FontAtlas; import :Font; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM import :Device; import :DescriptorHeapVulkan; import :ImageVulkan; import :VulkanBuffer; import :ComputeShader; #else import :DescriptorHeapWebGPU; import :WebGPU; import :WebGPUBuffer; import :WebGPUComputeShader; #endif export namespace Crafter { // ─── push-constant header ─────────────────────────────────────────── // Mirrors shaders/ui-shared.glsl::UIDispatchHeader byte-for-byte. User // shaders MUST embed this as the first member of their push-constant // struct so UIRenderer::FillHeader works. struct UIDispatchHeader { std::uint32_t outImage; std::uint32_t itemBuffer; std::uint32_t surfaceWidth; std::uint32_t surfaceHeight; float clipX, clipY, clipW, clipH; std::uint32_t itemCount; std::uint32_t frameIdx; std::uint32_t flags; std::uint32_t _pad; }; static_assert(sizeof(UIDispatchHeader) == 48); // ─── standard item PODs (match GLSL std430) ───────────────────────── struct QuadItem { float x, y, w, h; float r, g, b, a; float cTL, cTR, cBR, cBL; float outline, oR, oG, oB; }; static_assert(sizeof(QuadItem) == 64); struct CircleItem { float cx, cy, radius, _p0; float r, g, b, a; float outline, oR, oG, oB; }; static_assert(sizeof(CircleItem) == 48); struct ImageItem { float x, y, w, h; float u0, v0, u1, v1; float tR, tG, tB, tA; std::uint32_t texSlot, sampSlot, _p1, _p2; }; static_assert(sizeof(ImageItem) == 64); struct GlyphItem { float x, y, w, h; float u0, v0, u1, v1; float r, g, b, a; }; static_assert(sizeof(GlyphItem) == 48); // ─── tiny rect-carving helper ─────────────────────────────────────── struct Rect { float x = 0, y = 0, w = 0, h = 0; enum class Anchor { Top, Bottom, Left, Right }; Rect SubRect(float size, Anchor a) const noexcept { switch (a) { case Anchor::Top: return { x, y, w, std::min(size, h) }; case Anchor::Bottom: return { x, y + h - std::min(size, h), w, std::min(size, h) }; case Anchor::Left: return { x, y, std::min(size, w), h }; case Anchor::Right: return { x + w - std::min(size, w), y, std::min(size, w), h }; } return *this; } Rect Inset(float padding) const noexcept { return Inset(padding, padding, padding, padding); } Rect Inset(float top, float right, float bottom, float left) const noexcept { float nw = std::max(0.0f, w - left - right); float nh = std::max(0.0f, h - top - bottom); return { x + left, y + top, nw, nh }; } bool Contains(float px, float py) const noexcept { return px >= x && px < x + w && py >= y && py < y + h; } static Rect FromWindow(const Window& win) noexcept { return { 0, 0, static_cast(win.width), static_cast(win.height) }; } }; // ─── per-frame callback args ──────────────────────────────────────── struct UIBuildArgs { GraphicsCommandBuffer cmd; std::uint32_t frameIdx; }; // ─── UIRenderer ───────────────────────────────────────────────────── class UIRenderer : public RenderPass { public: GraphicsComputeShader drawQuads; GraphicsComputeShader drawCircles; GraphicsComputeShader drawImages; GraphicsComputeShader drawText; FontAtlas* fontAtlas = nullptr; Crafter::Event onBuild; UIRenderer() = default; UIRenderer(const UIRenderer&) = delete; UIRenderer& operator=(const UIRenderer&) = delete; void Initialize(Window& window, GraphicsDescriptorHeap& heap, GraphicsCommandBuffer initCmd, std::filesystem::path quadsSpv = "ui-quads.comp.spv", std::filesystem::path circlesSpv = "ui-circles.comp.spv", std::filesystem::path imagesSpv = "ui-images.comp.spv", std::filesystem::path textSpv = "ui-text.comp.spv"); void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override; UIDispatchHeader FillHeader(std::uint32_t itemBufferSlot, std::uint32_t itemCount, std::array clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f}, std::uint32_t flags = 0) const noexcept; void DispatchQuads(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount, std::array clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f}); void DispatchCircles(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount, std::array clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f}); void DispatchImages(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount, std::array clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f}); void DispatchText(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount, std::array clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f}); #ifndef CRAFTER_GRAPHICS_WINDOW_DOM // Generic dispatch — user-authored shaders. Vulkan-only in v1; on DOM // the WebGPU side has no bindless and would need per-shader bind-group // declaration. See plan section 3b for the design path. void Dispatch(GraphicsCommandBuffer cmd, const ComputeShader& shader, const void* push, std::uint32_t pushBytes, std::uint32_t gx, std::uint32_t gy = 1, std::uint32_t gz = 1); #endif // Allocates a heap slot for the buffer and registers the GPU handle. // Returns a move-only BufferSlot RAII handle. template BufferSlot RegisterBuffer(GraphicsBuffer& buffer); #ifndef CRAFTER_GRAPHICS_WINDOW_DOM template ImageSlot RegisterImage(ImageVulkan& image, VkFormat format, VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); SamplerSlot RegisterSampler(const VkSamplerCreateInfo& info); #endif SamplerSlot RegisterLinearClampSampler(); std::uint32_t ShapeText(Font& font, float pxSize, float x, float baselineY, std::string_view utf8, std::array color, GlyphItem* out, std::uint32_t outCapacity, float* outAdvance = nullptr); std::uint16_t FontAtlasImageSlot() const noexcept { return fontAtlasImageSlot_; } std::uint16_t FontAtlasSamplerSlot() const noexcept { return fontAtlasSamplerSlot_; } std::uint16_t OutImageSlot() const noexcept { return outImageSlot_; } private: Window* window_ = nullptr; GraphicsDescriptorHeap* heap_ = nullptr; ImageSlot outImageSlot_; ImageSlot fontAtlasImageSlot_; SamplerSlot fontAtlasSamplerSlot_; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM VkImageViewCreateInfo atlasViewCreateInfo_{}; bool firstDispatchThisFrame_ = true; #endif Crafter::EventListener resizeSub_; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM void WriteSwapchainDescriptors(); void WriteFontAtlasDescriptor(); void WriteBufferDescriptor(std::uint16_t slot, VkDeviceAddress address, std::uint32_t size); void WriteSampledImageDescriptor(std::uint16_t slot, const VkImageViewCreateInfo& viewInfo, VkImageLayout layout); #endif }; // ─── template-method implementations ──────────────────────────────── template BufferSlot UIRenderer::RegisterBuffer(GraphicsBuffer& buffer) { auto range = heap_->AllocateBufferSlots(1); #ifndef CRAFTER_GRAPHICS_WINDOW_DOM WriteBufferDescriptor(range.firstElement, buffer.address, buffer.size); #else heap_->bufferTable[range.firstElement] = buffer.handle; #endif return BufferSlot{heap_, range.firstElement}; } #ifndef CRAFTER_GRAPHICS_WINDOW_DOM template ImageSlot UIRenderer::RegisterImage(ImageVulkan& image, VkFormat format, VkImageLayout layout) { auto range = heap_->AllocateImageSlots(1); VkImageViewCreateInfo info { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = image.image, .viewType = VK_IMAGE_VIEW_TYPE_2D, .format = format, .components = { VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, }, .subresourceRange = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0, .levelCount = image.mipLevels, .baseArrayLayer = 0, .layerCount = 1, }, }; WriteSampledImageDescriptor(range.firstElement, info, layout); return ImageSlot{heap_, range.firstElement}; } #endif }