//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Issue #55: once the font atlas is full, a glyph ShapeText places can take a // cell that held another glyph, whose SDF the GPU image keeps until the atlas // is uploaded again. DispatchText and DispatchFused uploaded it before their // kernels; the generic UIRenderer::Dispatch, the Tier 1 path a custom text // kernel sampling FontAtlasImageSlot() takes, did not, so for that frame the // kernel drew the cell's old glyph. Every kernel the renderer dispatches now // uploads the atlas first. // // What is asserted, in one onBuild: ShapeText of new glyphs leaves the atlas // dirty; a ui.Dispatch of a custom kernel (the standard quads one, given no // items) leaves it clean, and the frame renders without validation errors // (which counts only with CRAFTER_GRAPHICS_VALIDATION set). // // Needs the Wayland backend and a live compositor (the UIRenderer needs a // Window), the UI kernels and a font, which project.cpp copies next to the // binary. #include "vulkan/vulkan.h" #include import Crafter.Graphics; import Crafter.Event; import std; using namespace Crafter; namespace fs = std::filesystem; namespace { int failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } fs::path ExeDir() { return fs::read_symlink("/proc/self/exe").parent_path(); } } // namespace int main() { const fs::path dir = ExeDir(); Device::Initialize(); Window window(320, 200, "UIDispatchAtlasFlush test"); VkCommandBuffer init = window.StartInit(); GraphicsDescriptorHeap heap; heap.Initialize(/*images*/ 4, /*buffers*/ 4, /*samplers*/ 2); window.descriptorHeap = &heap; Font font(dir / "font.ttf"); FontAtlas atlas; atlas.Initialize(init); UIRenderer ui; ui.fontAtlas = &atlas; ui.Initialize(window, heap, init, dir / "ui-quads.comp.spv", dir / "ui-circles.comp.spv", dir / "ui-images.comp.spv", dir / "ui-text.comp.spv", dir / "ui-fused.comp.spv"); window.passes.push_back(&ui); GraphicsBuffer quads; quads.Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 1); BufferSlot quadsSlot = ui.RegisterBuffer(quads); bool built = false, dirtyAfterShaping = false, dirtyAfterDispatch = true; EventListener buildSub(&ui.onBuild, [&](UIBuildArgs a) { if (built) return; built = true; std::array glyphs{}; ui.ShapeText(font, 24.0f, 10.0f, 40.0f, "Tier 1 kernels", {1, 1, 1, 1}, glyphs.data(), static_cast(glyphs.size())); dirtyAfterShaping = atlas.dirty; const UIDispatchHeader header = ui.FillHeader(quadsSlot, 0); ui.Dispatch(a.cmd, ui.drawQuads, &header, sizeof(header), (window.width + 7) / 8, (window.height + 7) / 8); dirtyAfterDispatch = atlas.dirty; }); window.FinishInit(); window.Render(); Check(built, "onBuild ran"); Check(dirtyAfterShaping, "shaping new glyphs leaves the atlas dirty"); Check(!dirtyAfterDispatch, "a custom kernel's ui.Dispatch uploads the atlas before it runs"); if (std::getenv("CRAFTER_GRAPHICS_VALIDATION")) { Check(Device::validationErrorCount == 0, std::format("no validation errors ({} seen)", Device::validationErrorCount)); } if (failures == 0) std::println("\nAll UI dispatch atlas flush checks passed."); else std::println("\n{} UI dispatch atlas flush check(s) FAILED.", failures); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }