/* Crafter®.Graphics Copyright (C) 2026 Catcrafts® catcrafts.net */ // Backend-agnostic UIRenderer methods. FillHeader and ShapeText do not // touch any GPU API; they only read window dimensions and (for ShapeText) // the CPU-side font atlas. Split out so both Vulkan and WebGPU impls // share the same source. module Crafter.Graphics:UI_shared_impl; import :UI; import :Window; import :Font; import :FontAtlas; import :GraphicsTypes; import std; using namespace Crafter; UIDispatchHeader UIRenderer::FillHeader(std::uint32_t itemBufferSlot, std::uint32_t itemCount, std::array clipRectPx, std::uint32_t flags) const noexcept { UIDispatchHeader h{}; h.outImage = outImageSlot_; h.itemBuffer = itemBufferSlot; h.surfaceWidth = window_->width; h.surfaceHeight = window_->height; h.clipX = clipRectPx[0]; h.clipY = clipRectPx[1]; h.clipW = clipRectPx[2]; h.clipH = clipRectPx[3]; h.itemCount = itemCount; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM h.frameIdx = window_->currentBuffer; #else h.frameIdx = 0; #endif h.flags = ClipFlags(clipRectPx, window_->width, window_->height, flags); h._pad = 0; return h; } std::uint32_t UIRenderer::ClipFlags(std::array clipRectPx, std::uint32_t surfaceWidth, std::uint32_t surfaceHeight, std::uint32_t flags) noexcept { // The reserved kUIFlagClip bit signals "clip rect is narrower than the // surface". When the rect already covers the whole surface (the common // {0,0,1e9,1e9} default), the four per-pixel clip compares in // uiResolveScreenPixel can never reject an in-surface pixel, so we leave // the bit clear and the shader skips them — pixel-identical output. const bool clipCoversSurface = clipRectPx[0] <= 0.0f && clipRectPx[1] <= 0.0f && clipRectPx[0] + clipRectPx[2] >= static_cast(surfaceWidth) && clipRectPx[1] + clipRectPx[3] >= static_cast(surfaceHeight); return clipCoversSurface ? (flags & ~kUIFlagClip) : (flags | kUIFlagClip); } std::uint32_t UIRenderer::ShapeText(Font& font, float pxSize, float x, float baselineY, std::string_view utf8, std::array color, GlyphItem* out, std::uint32_t outCapacity, float* outAdvance, TextAlign align) { if (fontAtlas == nullptr) { std::println("UIRenderer::ShapeText: no FontAtlas (set fontAtlas before Initialize)"); std::abort(); } // Look up (or build) the origin-relative shaped run. It is stored with // the pen starting at x=0, baseline=0 so a cache hit only needs the // translate below. The whole string is shaped (no outCapacity limit) so // a later call with a larger buffer still gets the full run. // Probe with a borrowing view key (text is a string_view onto utf8) so a // cache hit copies and hashes no string. The owning std::string is only // built on a miss, below, for the emplace. ShapedRunViewKey viewKey{&font, pxSize, color, utf8}; auto it = shapedRuns_.find(viewKey); if (it == shapedRuns_.end()) { // Miss: shape from scratch. Ensure() each glyph so brand-new ones // rasterise into the atlas and mark it dirty for the next upload — // a hit skips this, which is safe because atlas entries are never // evicted once placed. const float scale = pxSize / FontAtlas::kBaseSize; ShapedRun run; float cursor = 0.0f; std::size_t i = 0; while (i < utf8.size()) { std::uint32_t cp = DecodeUtf8(utf8, i); if (cp == 0) break; if (cp == '\n') { continue; } const Glyph* g = fontAtlas->EnsureAndGet(font, cp); if (g == nullptr) continue; if (g->w > 0 && g->h > 0) { GlyphItem gi; gi.x = cursor + g->xoff * scale; gi.y = g->yoff * scale; gi.w = g->w * scale; gi.h = g->h * scale; gi.u0 = g->u0; gi.v0 = g->v0; gi.u1 = g->u1; gi.v1 = g->v1; gi.r = color[0]; gi.g = color[1]; gi.b = color[2]; gi.a = color[3]; run.glyphs.push_back(gi); } cursor += g->advance * scale; } run.advance = cursor; if (shapedRuns_.size() >= kMaxShapedRuns) shapedRuns_.clear(); ShapedRunKey key{&font, pxSize, color, std::string(utf8)}; it = shapedRuns_.emplace(std::move(key), std::move(run)).first; } const ShapedRun& run = it->second; if (outAdvance) *outAdvance = run.advance; // Fold the alignment shift into the translate so callers don't need a // second pass over the glyphs. const float alignShift = (align == TextAlign::Center) ? -run.advance * 0.5f : (align == TextAlign::Right) ? -run.advance : 0.0f; const float tx = x + alignShift; std::uint32_t written = 0; for (const GlyphItem& src : run.glyphs) { if (written >= outCapacity) break; GlyphItem& gi = out[written++]; gi = src; gi.x += tx; gi.y += baselineY; } return written; } void UIRenderer::InvalidateFont(const Font& font) noexcept { std::erase_if(shapedRuns_, [&font](const auto& kv) { return kv.first.font == &font; }); }