The atlas packed glyphs onto shelves and never gave a place back (#55). Once its 1024x1024 image was full, EnsureAndGet returned nullptr for each new glyph before caching anything, so every new string holding one rasterised it again, and ShapeText dropped it together with its advance: the rest of the line closed up over the gap, and stayed that way. FontAtlas - A glyph is measured, not rasterised, until it gets a cell. One that finds no free room keeps its advance and box and draws blank for a frame. EnsureAndGet always returns the glyph, now a reference; Glyph::Resident() says whether it has a cell, Glyph::handle which one (slot and generation). - Evictions wait for the next frame: its first EnsureAndGet, Touch or Update places the glyphs that waited, once every glyph the frame before drew has been marked. Evicting while a frame is still asking for glyphs took ones it drew further down, which then took others in turn: a page switched back to could rasterise most of itself again rather than what it was missing, and on the DOM backend an over-full view could do so every frame. - A glyph keeps its cell for framesInFlight + 1 frames after it was drawn. The frame loop waits frame u's fence at the start of frame u + framesInFlight, but Device::frameCounter reads that from the end of the frame before, and input callbacks and onBeforeUpdate run in between. - FontAtlas::Packer places glyphs. Until the rows run out it fills shelves exactly as before. Then a glyph takes the same columns of one shelf at least as tall, or of several in a row as tall together, the free rows below the last shelf counting as one more: free texels, and, when the frame begins, glyphs nothing in flight draws, the least recently drawn first. A glyph spanning shelves lets in one taller than every shelf, and a few glyphs drawn every frame pin only their columns. Merging whole shelves instead, tried first, left a random workload without room for a glyph while a median 43% of the atlas was in flight; spanning, at 65%. - A box that found no room in a frame is remembered, with every box at least as big, as a small set per mode, until the frame ends or (without eviction) something is evicted: a saturated atlas no longer searches every shelf for each of its missing glyphs. - An evicted glyph is forgotten, and its texels are zeroed and uploaded with the glyph that takes its place: a neighbour's filtering at its edge must read padding or zero, not a stale glyph. - Touch(handles) marks every glyph whose handle still holds its cell, and says whether all did. InvalidateFont(font) forgets the font's glyphs and those waiting for a cell, whose Font* the next frame would read. The glyph index is looked up at rasterisation, so a Font replaced in place never gets one past its glyphs. GlyphsRasterized() counts, for tests. UIRenderer - A shaped run keeps a handle per glyph it draws. A hit touches them all, so the atlas keeps what is drawn every frame, and shapes the run again when one was evicted or one had no room. A glyph without room draws blank but advances the pen. - InvalidateFont clears the atlas's glyphs of the font too, as its comment and AGENTS.md always said it did. - Record calls the atlas's Update before onBuild, every frame, so the waiting glyphs are placed and uploaded first. Dispatch uploads the atlas before any kernel, a Tier 1 one sampling FontAtlasImageSlot() included: a glyph placed earlier in onBuild can sit in a cell another glyph had. DispatchText and DispatchFused no longer do it themselves. Device, Window (DOM) - Device::frameCounter and Device::framesInFlight exist on the DOM backend too, the counter bumped at the end of Window::Render, with one frame in flight: a frame's writeTexture lands before that frame's submit but after every earlier one. HelloUI - One set of item buffers and slots per frame in flight, by frameIdx. A frame still on the GPU read the items the next frame's onBuild was rewriting; with cells changing hands, those can name cells it drew other glyphs in. Tests - FontAtlasPacker: the packer with made-up boxes and no device: the fill order, least recently drawn first, framesInFlight + 1 frames held, nothing without evict, wide glyphs, tall glyphs spanning shelves and the free rows, Touch past a stale handle, stale handles, dirty spans, and the invariants over a random workload placed the way the atlas places. - FontAtlasEviction: fills the atlas through ShapeText frame by frame. The line that no longer fits keeps its advance and its glyphs' places; its missing glyphs aren't rasterised while nothing can go; when only line 0 is cold, only its glyphs give way; a frame's own misses evict nothing in it; a frame later the line and twenty more are whole, pixel for pixel, line 0 losing a larger share than the glyphs drawn after it; a stale cached run is shaped again; no stale texel is left; InvalidateFont reaches the atlas. Against master, with its two calls to the new API stubbed out, an earlier version failed ten checks. - UIDispatchAtlasFlush: a custom ui.Dispatch after ShapeText leaves the atlas uploaded. Needs a compositor. - ShapeTextCache: a hit rasterises nothing; after InvalidateFont the re-shaped run draws the same, from new cells. Docs - README and AGENTS.consumer.md: the atlas's eviction, the frame a glyph waits, shaping text every frame into per-frame item buffers, InvalidateFont, EnsureAndGet's signature and Lookup's lifetime, and that the atlas, like the deletion queue, assumes one rendering window and evicts nothing without one. - AGENTS.md: FontAtlas::Packer among the pure-logic helpers, eviction without a Window, and InvalidateFont. Breaking: FontAtlas::EnsureAndGet returns const Glyph& and never fails; check Glyph::Resident() before drawing from its UVs. FontAtlas::Touch is no longer noexcept. A GlyphItem is good for the frame it was shaped for, once the atlas has filled up, and item buffers must be per frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
404 lines
18 KiB
C++
404 lines
18 KiB
C++
//SPDX-License-Identifier: LGPL-3.0-only
|
||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
||
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;
|
||
import :GraphicsTypes;
|
||
import std;
|
||
|
||
using namespace Crafter;
|
||
|
||
// ─── Initialize ─────────────────────────────────────────────────────────
|
||
|
||
void UIRenderer::Initialize(Window& window, GraphicsDescriptorHeap& heap, GraphicsCommandBuffer initCmd,
|
||
std::filesystem::path quadsSpv,
|
||
std::filesystem::path circlesSpv,
|
||
std::filesystem::path imagesSpv,
|
||
std::filesystem::path textSpv,
|
||
std::filesystem::path fusedSpv) {
|
||
window_ = &window;
|
||
heap_ = &heap;
|
||
|
||
// Load the four standard pipelines plus the fused uber-kernel (issue #47).
|
||
drawQuads.Load(quadsSpv, heap);
|
||
drawCircles.Load(circlesSpv, heap);
|
||
drawImages.Load(imagesSpv, heap);
|
||
drawText.Load(textSpv, heap);
|
||
drawFused.Load(fusedSpv, heap);
|
||
|
||
// One image slot for the swapchain output. The window writes the image
|
||
// each frame draws into there, in that frame's heap copy (or set), so it
|
||
// follows resizes and present-mode changes by itself; called during a
|
||
// frame, it is written for that frame too.
|
||
auto outRange = heap_->AllocateImageSlots(1);
|
||
outImageSlot_ = ImageSlot{heap_, outRange.firstElement};
|
||
heap_->AddSwapchainSlot(outRange.firstElement, window);
|
||
|
||
// 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);
|
||
fontAtlasImageSlot_ = ImageSlot{heap_, atlasImg.firstElement};
|
||
fontAtlasSamplerSlot_ = RegisterLinearClampSampler();
|
||
WriteFontAtlasDescriptor();
|
||
}
|
||
|
||
// No whole-heap flush here: WriteFontAtlasDescriptor / RegisterSampler each
|
||
// flush their own written byte range above, so the GPU already sees
|
||
// everything written during setup.
|
||
|
||
(void)initCmd; // reserved for future image-layout tweaks
|
||
}
|
||
|
||
// ─── per-frame Record ───────────────────────────────────────────────────
|
||
|
||
void UIRenderer::Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) {
|
||
// Reset per-frame state.
|
||
firstDispatchThisFrame_ = true;
|
||
|
||
// The atlas's first call of the frame: it gives the glyphs the last frame
|
||
// found no free room for their cells, evicting what that frame did not
|
||
// draw (#55), and uploads them with whatever else is pending, before
|
||
// onBuild shapes anything. Glyphs placed during onBuild are uploaded by
|
||
// Dispatch, before the kernel that may sample them.
|
||
if (fontAtlas != nullptr) {
|
||
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.
|
||
|
||
void UIRenderer::DispatchQuads(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
|
||
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);
|
||
}
|
||
|
||
void UIRenderer::DispatchCircles(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
|
||
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);
|
||
}
|
||
|
||
void UIRenderer::DispatchImages(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
|
||
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);
|
||
}
|
||
|
||
void UIRenderer::DispatchText(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
|
||
std::uint32_t itemCount,
|
||
std::array<float,4> clipRectPx) {
|
||
if (itemCount == 0) return;
|
||
if (!fontAtlasImageSlot_) {
|
||
throw std::runtime_error("UIRenderer::DispatchText: no FontAtlas registered (set fontAtlas before Initialize)");
|
||
}
|
||
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);
|
||
}
|
||
|
||
// ─── 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)");
|
||
}
|
||
|
||
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);
|
||
pc.surfaceWidth = window_->width;
|
||
pc.surfaceHeight = window_->height;
|
||
pc.frameIdx = window_->currentFrame;
|
||
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);
|
||
}
|
||
|
||
// ─── generic Dispatch (with barrier) ────────────────────────────────────
|
||
|
||
void UIRenderer::Dispatch(GraphicsCommandBuffer cmd, const GraphicsComputeShader& shader,
|
||
const void* push, std::uint32_t pushBytes,
|
||
std::uint32_t gx, std::uint32_t gy, std::uint32_t gz) {
|
||
// Upload the glyphs ShapeText placed earlier in this onBuild before any
|
||
// kernel runs, the text ones and a Tier 1 one sampling the atlas through
|
||
// FontAtlasImageSlot alike: a glyph can sit in a cell another glyph had
|
||
// (#55), which would otherwise show that glyph for the frame.
|
||
if (fontAtlas != nullptr && fontAtlas->dirty) {
|
||
fontAtlas->Update(cmd);
|
||
}
|
||
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_->currentImage],
|
||
.subresourceRange = {
|
||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
||
.baseMipLevel = 0,
|
||
.levelCount = 1,
|
||
.baseArrayLayer = 0,
|
||
.layerCount = 1,
|
||
},
|
||
};
|
||
vkCmdPipelineBarrier(cmd,
|
||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||
0, 0, nullptr, 0, nullptr, 1, &ib);
|
||
}
|
||
firstDispatchThisFrame_ = false;
|
||
shader.Dispatch(cmd, push, pushBytes, gx, gy, gz);
|
||
}
|
||
|
||
// ─── descriptor writes ─────────────────────────────────────────────────
|
||
|
||
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) {
|
||
if (!Device::descriptorHeapSupported) {
|
||
heap_->WriteImageSets(slot, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, viewInfo, layout);
|
||
return;
|
||
}
|
||
|
||
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()
|
||
);
|
||
// One image descriptor written per heap, at the same slot offset — flush
|
||
// just that range rather than the whole heap.
|
||
const std::uint32_t off = heap_->ImageByteOffset(slot);
|
||
const std::uint32_t sz = Device::descriptorHeapProperties.imageDescriptorSize;
|
||
for (auto& h : heap_->resourceHeap) h.FlushDevice(off, sz);
|
||
}
|
||
|
||
void UIRenderer::WriteBufferDescriptor(std::uint16_t slot, VkBuffer buffer, VkDeviceAddress address, std::uint32_t size) {
|
||
if (!Device::descriptorHeapSupported) {
|
||
// A descriptor set names the buffer by handle; the heap below names
|
||
// it by device address.
|
||
heap_->WriteBufferSets(slot, buffer);
|
||
return;
|
||
}
|
||
|
||
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()
|
||
);
|
||
// One buffer descriptor written per heap, at the same slot offset — flush
|
||
// just that range rather than the whole heap.
|
||
const std::uint32_t off = heap_->BufferByteOffset(slot);
|
||
const std::uint32_t sz = Device::descriptorHeapProperties.bufferDescriptorSize;
|
||
for (auto& h : heap_->resourceHeap) h.FlushDevice(off, sz);
|
||
}
|
||
|
||
SamplerSlot UIRenderer::RegisterSampler(const VkSamplerCreateInfo& info) {
|
||
auto range = heap_->AllocateSamplerSlots(1);
|
||
if (!Device::descriptorHeapSupported) {
|
||
// The handle owns the slot before the sampler is created, so a
|
||
// failed vkCreateSampler returns it to the freelist.
|
||
SamplerSlot slot{heap_, range.firstElement};
|
||
heap_->WriteSamplerSets(range.firstElement, info);
|
||
return slot;
|
||
}
|
||
|
||
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()
|
||
);
|
||
// One sampler descriptor written per heap, at the same slot offset — flush
|
||
// just that range rather than the whole sampler heap.
|
||
const std::uint32_t off = heap_->SamplerByteOffset(range.firstElement);
|
||
const std::uint32_t sz = Device::descriptorHeapProperties.samplerDescriptorSize;
|
||
for (auto& h : heap_->samplerHeap) h.FlushDevice(off, sz);
|
||
return SamplerSlot{heap_, range.firstElement};
|
||
}
|
||
|
||
SamplerSlot UIRenderer::RegisterLinearClampSampler() {
|
||
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);
|
||
}
|
||
|