Crafter.Graphics/implementations/Crafter.Graphics-UI.cpp

428 lines
19 KiB
C++
Raw Normal View History

2026-05-02 21:08:20 +02:00
/*
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;
#include "vulkan/vulkan.h"
module Crafter.Graphics:UI_impl;
import :UI;
import :ComputeShader;
import :Device;
import :Window;
import :DescriptorHeapVulkan;
import :ImageVulkan;
import :VulkanBuffer;
import :FontAtlas;
import :Font;
2026-05-18 04:58:52 +02:00
import :GraphicsTypes;
2026-05-02 21:08:20 +02:00
import std;
using namespace Crafter;
// ─── Initialize ─────────────────────────────────────────────────────────
2026-05-18 04:58:52 +02:00
void UIRenderer::Initialize(Window& window, GraphicsDescriptorHeap& heap, GraphicsCommandBuffer initCmd,
2026-05-02 21:08:20 +02:00
std::filesystem::path quadsSpv,
std::filesystem::path circlesSpv,
std::filesystem::path imagesSpv,
perf(ui): add additive DispatchFused to collapse consecutive UI passes (#47) Add a fifth UI compute path — DispatchFused — alongside the four per-element Dispatch* calls. It composites quads → circles → images → text (canonical back-to-front order) in ONE dispatch that loads the destination image once and stores once, eliminating the per-pass load+store and the inter-pass VkMemoryBarriers a run of consecutive Dispatch* calls would pay. The win materialises at >= 2 non-empty categories; an absent category is a zero-trip, push-constant-uniform loop (~free). The new uber-kernel (shaders/ui-fused.comp.glsl) reuses each category's exact cooperative shared-memory tile-cull + per-pixel accumulate, so a fused category is pixel-identical to its standalone pass. Categories run sequentially per thread and share one set of shared-memory scratch, so the VGPR/shared high-water mark stays at the per-category level, not the sum. Additive and non-breaking: the per-element Dispatch* calls and the frozen 48-byte UIDispatchHeader are untouched. DispatchFused gets its own 128-byte push-constant layout (UIFusedHeader: four uvec4 headers + four per-category clip vec4s). To interleave a custom ui.Dispatch() between standard passes, bracket it with two DispatchFused calls — the dispatch boundary is the explicit, app-declared flush point. WebGPU (Vulkan-second): DispatchFused falls back to the per-element Dispatch* calls in canonical order — one texture/sampler per dispatch there can't feed the bindless image phase — so the result matches; only the load/store fusion is Vulkan-only. HelloUI now composites its background quads, mouse circle, and label text in a single DispatchFused. Verified rendering identical on a live Vulkan/Wayland session. Resolves #47 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:03:24 +00:00
std::filesystem::path textSpv,
std::filesystem::path fusedSpv) {
2026-05-02 21:08:20 +02:00
window_ = &window;
heap_ = &heap;
perf(ui): add additive DispatchFused to collapse consecutive UI passes (#47) Add a fifth UI compute path — DispatchFused — alongside the four per-element Dispatch* calls. It composites quads → circles → images → text (canonical back-to-front order) in ONE dispatch that loads the destination image once and stores once, eliminating the per-pass load+store and the inter-pass VkMemoryBarriers a run of consecutive Dispatch* calls would pay. The win materialises at >= 2 non-empty categories; an absent category is a zero-trip, push-constant-uniform loop (~free). The new uber-kernel (shaders/ui-fused.comp.glsl) reuses each category's exact cooperative shared-memory tile-cull + per-pixel accumulate, so a fused category is pixel-identical to its standalone pass. Categories run sequentially per thread and share one set of shared-memory scratch, so the VGPR/shared high-water mark stays at the per-category level, not the sum. Additive and non-breaking: the per-element Dispatch* calls and the frozen 48-byte UIDispatchHeader are untouched. DispatchFused gets its own 128-byte push-constant layout (UIFusedHeader: four uvec4 headers + four per-category clip vec4s). To interleave a custom ui.Dispatch() between standard passes, bracket it with two DispatchFused calls — the dispatch boundary is the explicit, app-declared flush point. WebGPU (Vulkan-second): DispatchFused falls back to the per-element Dispatch* calls in canonical order — one texture/sampler per dispatch there can't feed the bindless image phase — so the result matches; only the load/store fusion is Vulkan-only. HelloUI now composites its background quads, mouse circle, and label text in a single DispatchFused. Verified rendering identical on a live Vulkan/Wayland session. Resolves #47 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:03:24 +00:00
// Load the four standard pipelines plus the fused uber-kernel (issue #47).
2026-05-02 21:08:20 +02:00
drawQuads.Load(quadsSpv);
drawCircles.Load(circlesSpv);
drawImages.Load(imagesSpv);
drawText.Load(textSpv);
perf(ui): add additive DispatchFused to collapse consecutive UI passes (#47) Add a fifth UI compute path — DispatchFused — alongside the four per-element Dispatch* calls. It composites quads → circles → images → text (canonical back-to-front order) in ONE dispatch that loads the destination image once and stores once, eliminating the per-pass load+store and the inter-pass VkMemoryBarriers a run of consecutive Dispatch* calls would pay. The win materialises at >= 2 non-empty categories; an absent category is a zero-trip, push-constant-uniform loop (~free). The new uber-kernel (shaders/ui-fused.comp.glsl) reuses each category's exact cooperative shared-memory tile-cull + per-pixel accumulate, so a fused category is pixel-identical to its standalone pass. Categories run sequentially per thread and share one set of shared-memory scratch, so the VGPR/shared high-water mark stays at the per-category level, not the sum. Additive and non-breaking: the per-element Dispatch* calls and the frozen 48-byte UIDispatchHeader are untouched. DispatchFused gets its own 128-byte push-constant layout (UIFusedHeader: four uvec4 headers + four per-category clip vec4s). To interleave a custom ui.Dispatch() between standard passes, bracket it with two DispatchFused calls — the dispatch boundary is the explicit, app-declared flush point. WebGPU (Vulkan-second): DispatchFused falls back to the per-element Dispatch* calls in canonical order — one texture/sampler per dispatch there can't feed the bindless image phase — so the result matches; only the load/store fusion is Vulkan-only. HelloUI now composites its background quads, mouse circle, and label text in a single DispatchFused. Verified rendering identical on a live Vulkan/Wayland session. Resolves #47 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:03:24 +00:00
drawFused.Load(fusedSpv);
2026-05-02 21:08:20 +02:00
// Allocate one image slot for the swapchain output. Each per-frame heap
// copy will hold ITS frame's image at this slot.
auto outRange = heap_->AllocateImageSlots(1);
2026-05-05 00:02:04 +02:00
outImageSlot_ = ImageSlot{heap_, outRange.firstElement};
2026-05-02 21:08:20 +02:00
WriteSwapchainDescriptors();
// Optional font-atlas registration (user must have called atlas->Initialize
// already before reaching here, so atlas->image is live).
if (fontAtlas != nullptr) {
auto atlasImg = heap_->AllocateImageSlots(1);
2026-05-05 00:02:04 +02:00
fontAtlasImageSlot_ = ImageSlot{heap_, atlasImg.firstElement};
2026-05-02 21:08:20 +02:00
fontAtlasSamplerSlot_ = RegisterLinearClampSampler();
WriteFontAtlasDescriptor();
}
// Flush the host-mapped descriptor heaps so the GPU sees what we wrote.
for (auto& h : heap_->resourceHeap) h.FlushDevice();
for (auto& h : heap_->samplerHeap) h.FlushDevice();
2026-05-12 00:24:48 +02:00
// Re-bind the swapchain image descriptors after every resize. Window
// already drained the queue and rebuilt the imageViews[] before
// firing the event, so vkWriteResourceDescriptorsEXT is safe here.
resizeSub_.SetEvent(&window.onResize, [this]() {
WriteSwapchainDescriptors();
for (auto& h : heap_->resourceHeap) h.FlushDevice();
});
2026-05-02 21:08:20 +02:00
(void)initCmd; // reserved for future image-layout tweaks
}
// ─── per-frame Record ───────────────────────────────────────────────────
2026-05-18 04:58:52 +02:00
void UIRenderer::Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) {
2026-05-02 21:08:20 +02:00
// Reset per-frame state.
firstDispatchThisFrame_ = true;
// If text is in use, flush any glyphs that user-side ShapeText calls
// produced during a previous frame's onBuild. (Ensure() during the
// current onBuild also marks the atlas dirty; that's flushed on the
// NEXT Record. For v1 this is fine because the current frame's text
// dispatch reads whatever's already been uploaded, and brand-new glyphs
// missing from the atlas this frame will simply render blank for one
// frame and resolve next frame. To get them this frame, the user can
// call atlas->Update(cmd) themselves at the top of onBuild.)
if (fontAtlas != nullptr && fontAtlas->dirty) {
fontAtlas->Update(cmd);
}
onBuild.Invoke({cmd, frameIdx});
(void)window;
}
// ─── group-count helper ────────────────────────────────────────────────
namespace {
// Number of 8-pixel tiles needed to cover `dim` pixels (rounded up).
inline std::uint32_t TilesFor(std::uint32_t dim) {
return (dim + 7u) / 8u;
}
}
// ─── standard-shader convenience dispatches ─────────────────────────────
//
// All four standard shaders use the same pixel-tile dispatch model: one
// workgroup per 8×8 screen tile, each thread iterates every item in order
// inside the workgroup, accumulating into a local register. This guarantees
// "items in the buffer render in order" (later items overdraw earlier ones)
// without inter-workgroup races on imageLoad/imageStore — the bug that the
// per-item dispatch model had.
2026-05-18 04:58:52 +02:00
void UIRenderer::DispatchQuads(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
2026-05-02 21:08:20 +02:00
std::uint32_t itemCount,
std::array<float,4> clipRectPx) {
if (itemCount == 0) return;
struct PC { UIDispatchHeader hdr; } pc { FillHeader(bufferSlot, itemCount, clipRectPx) };
Dispatch(cmd, drawQuads, &pc, sizeof(pc),
TilesFor(window_->width), TilesFor(window_->height), 1u);
}
2026-05-18 04:58:52 +02:00
void UIRenderer::DispatchCircles(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
2026-05-02 21:08:20 +02:00
std::uint32_t itemCount,
std::array<float,4> clipRectPx) {
if (itemCount == 0) return;
struct PC { UIDispatchHeader hdr; } pc { FillHeader(bufferSlot, itemCount, clipRectPx) };
Dispatch(cmd, drawCircles, &pc, sizeof(pc),
TilesFor(window_->width), TilesFor(window_->height), 1u);
}
2026-05-18 04:58:52 +02:00
void UIRenderer::DispatchImages(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
2026-05-02 21:08:20 +02:00
std::uint32_t itemCount,
std::array<float,4> clipRectPx) {
if (itemCount == 0) return;
struct PC { UIDispatchHeader hdr; } pc { FillHeader(bufferSlot, itemCount, clipRectPx) };
Dispatch(cmd, drawImages, &pc, sizeof(pc),
TilesFor(window_->width), TilesFor(window_->height), 1u);
}
2026-05-18 04:58:52 +02:00
void UIRenderer::DispatchText(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
2026-05-02 21:08:20 +02:00
std::uint32_t itemCount,
std::array<float,4> clipRectPx) {
if (itemCount == 0) return;
2026-05-05 00:02:04 +02:00
if (!fontAtlasImageSlot_) {
2026-05-02 21:08:20 +02:00
throw std::runtime_error("UIRenderer::DispatchText: no FontAtlas registered (set fontAtlas before Initialize)");
}
// Flush any glyphs that ShapeText calls (during this onBuild) just
// rasterised, so the dispatch below sees them.
if (fontAtlas != nullptr && fontAtlas->dirty) {
fontAtlas->Update(cmd);
}
struct PC {
UIDispatchHeader hdr;
std::uint32_t fontTextureSlot;
std::uint32_t fontSamplerSlot;
std::uint32_t _p0;
std::uint32_t _p1;
} pc {
FillHeader(bufferSlot, itemCount, clipRectPx),
fontAtlasImageSlot_,
fontAtlasSamplerSlot_,
0, 0
};
Dispatch(cmd, drawText, &pc, sizeof(pc),
TilesFor(window_->width), TilesFor(window_->height), 1u);
}
perf(ui): add additive DispatchFused to collapse consecutive UI passes (#47) Add a fifth UI compute path — DispatchFused — alongside the four per-element Dispatch* calls. It composites quads → circles → images → text (canonical back-to-front order) in ONE dispatch that loads the destination image once and stores once, eliminating the per-pass load+store and the inter-pass VkMemoryBarriers a run of consecutive Dispatch* calls would pay. The win materialises at >= 2 non-empty categories; an absent category is a zero-trip, push-constant-uniform loop (~free). The new uber-kernel (shaders/ui-fused.comp.glsl) reuses each category's exact cooperative shared-memory tile-cull + per-pixel accumulate, so a fused category is pixel-identical to its standalone pass. Categories run sequentially per thread and share one set of shared-memory scratch, so the VGPR/shared high-water mark stays at the per-category level, not the sum. Additive and non-breaking: the per-element Dispatch* calls and the frozen 48-byte UIDispatchHeader are untouched. DispatchFused gets its own 128-byte push-constant layout (UIFusedHeader: four uvec4 headers + four per-category clip vec4s). To interleave a custom ui.Dispatch() between standard passes, bracket it with two DispatchFused calls — the dispatch boundary is the explicit, app-declared flush point. WebGPU (Vulkan-second): DispatchFused falls back to the per-element Dispatch* calls in canonical order — one texture/sampler per dispatch there can't feed the bindless image phase — so the result matches; only the load/store fusion is Vulkan-only. HelloUI now composites its background quads, mouse circle, and label text in a single DispatchFused. Verified rendering identical on a live Vulkan/Wayland session. Resolves #47 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:03:24 +00:00
// ─── fused dispatch (issue #47) ─────────────────────────────────────────
//
// One uber-kernel composites quads → circles → images → text into the output
// image with a single load and a single store. Routed through the generic
// Dispatch() so it still gets the standard write-after-write barrier when it
// is not the first dispatch of the frame, and a following dispatch barriers
// against it — the only barriers it removes are the INTRA-fuse ones a run of
// Dispatch* calls would have inserted between its categories.
void UIRenderer::DispatchFused(GraphicsCommandBuffer cmd,
const FusedBatch& quads,
const FusedBatch& circles,
const FusedBatch& images,
const FusedBatch& text) {
// Nothing to draw → no dispatch (and, crucially, no barrier bump).
if (quads.itemCount == 0 && circles.itemCount == 0 &&
images.itemCount == 0 && text.itemCount == 0) {
return;
}
if (text.itemCount != 0 && !fontAtlasImageSlot_) {
throw std::runtime_error("UIRenderer::DispatchFused: text batch given but no FontAtlas registered (set fontAtlas before Initialize)");
}
// Flush any glyphs ShapeText rasterised during this onBuild, mirroring
// DispatchText — only relevant when there is text to draw.
if (text.itemCount != 0 && fontAtlas != nullptr && fontAtlas->dirty) {
fontAtlas->Update(cmd);
}
UIFusedHeader pc{};
pc.itemBuffers[0] = quads.bufferSlot;
pc.itemBuffers[1] = circles.bufferSlot;
pc.itemBuffers[2] = images.bufferSlot;
pc.itemBuffers[3] = text.bufferSlot;
pc.itemCounts[0] = quads.itemCount;
pc.itemCounts[1] = circles.itemCount;
pc.itemCounts[2] = images.itemCount;
pc.itemCounts[3] = text.itemCount;
pc.outImage = outImageSlot_;
pc.fontTexture = fontAtlasImageSlot_;
pc.fontSampler = fontAtlasSamplerSlot_;
// Per-category clip-active bits, reusing the same surface-coverage test the
// standard path uses (ClipFlags). A category whose clip already covers the
// surface clears its bit so the kernel skips that category's per-pixel clip
// compares (the common full-surface case) — pixel-identical either way.
auto clipBit = [&](const FusedBatch& b, std::uint32_t bit) -> std::uint32_t {
return (ClipFlags(b.clipRectPx, window_->width, window_->height, 0) & kUIFlagClip) ? bit : 0u;
};
pc.flags = clipBit(quads, 0x1u) | clipBit(circles, 0x2u)
| clipBit(images, 0x4u) | clipBit(text, 0x8u);
perf(ui): add additive DispatchFused to collapse consecutive UI passes (#47) Add a fifth UI compute path — DispatchFused — alongside the four per-element Dispatch* calls. It composites quads → circles → images → text (canonical back-to-front order) in ONE dispatch that loads the destination image once and stores once, eliminating the per-pass load+store and the inter-pass VkMemoryBarriers a run of consecutive Dispatch* calls would pay. The win materialises at >= 2 non-empty categories; an absent category is a zero-trip, push-constant-uniform loop (~free). The new uber-kernel (shaders/ui-fused.comp.glsl) reuses each category's exact cooperative shared-memory tile-cull + per-pixel accumulate, so a fused category is pixel-identical to its standalone pass. Categories run sequentially per thread and share one set of shared-memory scratch, so the VGPR/shared high-water mark stays at the per-category level, not the sum. Additive and non-breaking: the per-element Dispatch* calls and the frozen 48-byte UIDispatchHeader are untouched. DispatchFused gets its own 128-byte push-constant layout (UIFusedHeader: four uvec4 headers + four per-category clip vec4s). To interleave a custom ui.Dispatch() between standard passes, bracket it with two DispatchFused calls — the dispatch boundary is the explicit, app-declared flush point. WebGPU (Vulkan-second): DispatchFused falls back to the per-element Dispatch* calls in canonical order — one texture/sampler per dispatch there can't feed the bindless image phase — so the result matches; only the load/store fusion is Vulkan-only. HelloUI now composites its background quads, mouse circle, and label text in a single DispatchFused. Verified rendering identical on a live Vulkan/Wayland session. Resolves #47 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:03:24 +00:00
pc.surfaceWidth = window_->width;
pc.surfaceHeight = window_->height;
pc.frameIdx = window_->currentBuffer;
pc._pad = 0;
std::copy(quads.clipRectPx.begin(), quads.clipRectPx.end(), pc.clipQuads);
std::copy(circles.clipRectPx.begin(), circles.clipRectPx.end(), pc.clipCircles);
std::copy(images.clipRectPx.begin(), images.clipRectPx.end(), pc.clipImages);
std::copy(text.clipRectPx.begin(), text.clipRectPx.end(), pc.clipText);
Dispatch(cmd, drawFused, &pc, sizeof(pc),
TilesFor(window_->width), TilesFor(window_->height), 1u);
}
2026-05-02 21:08:20 +02:00
// ─── generic Dispatch (with barrier) ────────────────────────────────────
2026-05-18 05:39:17 +02:00
void UIRenderer::Dispatch(GraphicsCommandBuffer cmd, const GraphicsComputeShader& shader,
2026-05-02 21:08:20 +02:00
const void* push, std::uint32_t pushBytes,
std::uint32_t gx, std::uint32_t gy, std::uint32_t gz) {
if (!firstDispatchThisFrame_) {
// Every UI pass read-modify-writes the same swapchain storage image
// (later passes overdraw earlier ones), so each dispatch must observe
// the previous dispatch's writes. The only resource the hazard touches
// is that one image, so scope the dependency to its subresource with a
// VkImageMemoryBarrier rather than a queue-wide VkMemoryBarrier: same
// correctness, but only this image's shader caches are flushed —
// unrelated buffers/images are no longer invalidated. The image stays
// in VK_IMAGE_LAYOUT_GENERAL (bound as a storage image), so this is a
// pure memory dependency, no layout transition.
//
// The execution dependency is still COMPUTE->COMPUTE over the whole
// queue, so the passes do NOT overlap — this is a narrower cache flush
// only. Eliminating the barriers entirely requires fusing the passes
// into one dispatch (tracked in #47); do not attempt that here.
VkImageMemoryBarrier ib {
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = window_->images[window_->currentBuffer],
.subresourceRange = {
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
2026-05-02 21:08:20 +02:00
};
vkCmdPipelineBarrier(cmd,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
0, 0, nullptr, 0, nullptr, 1, &ib);
2026-05-02 21:08:20 +02:00
}
firstDispatchThisFrame_ = false;
shader.Dispatch(cmd, push, pushBytes, gx, gy, gz);
}
// ─── descriptor writes ─────────────────────────────────────────────────
void UIRenderer::WriteSwapchainDescriptors() {
// Each per-frame heap holds ITS swapchain image at outImageSlot_.
std::array<VkImageDescriptorInfoEXT, Window::numFrames> infos{};
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
infos[f] = {
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
.pView = &window_->imageViews[f],
.layout = VK_IMAGE_LAYOUT_GENERAL,
};
resources[f] = {
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
.data = { .pImage = &infos[f] },
};
destinations[f] = {
.address = heap_->resourceHeap[f].value
+ heap_->ImageByteOffset(outImageSlot_),
.size = Device::descriptorHeapProperties.imageDescriptorSize,
};
}
Device::vkWriteResourceDescriptorsEXT(
Device::device, Window::numFrames, resources.data(), destinations.data()
);
}
void UIRenderer::WriteFontAtlasDescriptor() {
atlasViewCreateInfo_ = {
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.image = fontAtlas->image.image,
.viewType = VK_IMAGE_VIEW_TYPE_2D,
.format = VK_FORMAT_R8_UNORM,
.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 = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
};
WriteSampledImageDescriptor(fontAtlasImageSlot_,
atlasViewCreateInfo_,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
}
void UIRenderer::WriteSampledImageDescriptor(std::uint16_t slot,
const VkImageViewCreateInfo& viewInfo,
VkImageLayout layout) {
std::array<VkImageDescriptorInfoEXT, Window::numFrames> infos{};
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
infos[f] = {
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
.pView = &viewInfo,
.layout = layout,
};
resources[f] = {
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
.type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
.data = { .pImage = &infos[f] },
};
destinations[f] = {
.address = heap_->resourceHeap[f].value + heap_->ImageByteOffset(slot),
.size = Device::descriptorHeapProperties.imageDescriptorSize,
};
}
Device::vkWriteResourceDescriptorsEXT(
Device::device, Window::numFrames, resources.data(), destinations.data()
);
for (auto& h : heap_->resourceHeap) h.FlushDevice();
}
void UIRenderer::WriteBufferDescriptor(std::uint16_t slot, VkDeviceAddress address, std::uint32_t size) {
std::array<VkDeviceAddressRangeEXT, Window::numFrames> ranges{};
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
ranges[f] = { .address = address, .size = size };
resources[f] = {
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.data = { .pAddressRange = &ranges[f] },
};
destinations[f] = {
.address = heap_->resourceHeap[f].value + heap_->BufferByteOffset(slot),
.size = Device::descriptorHeapProperties.bufferDescriptorSize,
};
}
Device::vkWriteResourceDescriptorsEXT(
Device::device, Window::numFrames, resources.data(), destinations.data()
);
for (auto& h : heap_->resourceHeap) h.FlushDevice();
}
2026-05-05 00:02:04 +02:00
SamplerSlot UIRenderer::RegisterSampler(const VkSamplerCreateInfo& info) {
2026-05-02 21:08:20 +02:00
auto range = heap_->AllocateSamplerSlots(1);
std::array<VkSamplerCreateInfo, Window::numFrames> infos{};
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
infos[f] = info;
destinations[f] = {
.address = heap_->samplerHeap[f].value + heap_->SamplerByteOffset(range.firstElement),
.size = Device::descriptorHeapProperties.samplerDescriptorSize,
};
}
Device::vkWriteSamplerDescriptorsEXT(
Device::device, Window::numFrames, infos.data(), destinations.data()
);
for (auto& h : heap_->samplerHeap) h.FlushDevice();
2026-05-05 00:02:04 +02:00
return SamplerSlot{heap_, range.firstElement};
2026-05-02 21:08:20 +02:00
}
2026-05-05 00:02:04 +02:00
SamplerSlot UIRenderer::RegisterLinearClampSampler() {
2026-05-02 21:08:20 +02:00
VkSamplerCreateInfo s {
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
.magFilter = VK_FILTER_LINEAR,
.minFilter = VK_FILTER_LINEAR,
.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR,
.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.maxAnisotropy = 1.0f,
.minLod = 0.0f,
.maxLod = VK_LOD_CLAMP_NONE,
};
return RegisterSampler(s);
}