/* 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; // Evict the least-recently-used run (LRU list back) on overflow rather // than clearing the whole cache. The back is whatever has gone longest // without a reshape — a stale unique string, never a stable label // (those are reshaped every frame and sit at the front), so the hot set // survives and the next frame doesn't trigger a full-UI reshape. if (shapedRuns_.size() >= kMaxShapedRuns) { const ShapedRunKey* victim = shapedRunsLru_.back(); shapedRunsLru_.pop_back(); shapedRuns_.erase(*victim); } // Materialise the owning key (the probe above borrowed utf8) only now, // on the miss path, for the emplace. ShapedRunKey key{&font, pxSize, color, std::string(utf8)}; it = shapedRuns_.emplace(std::move(key), std::move(run)).first; it->second.lruIt = shapedRunsLru_.insert(shapedRunsLru_.begin(), &it->first); } else { // Hit: promote to most-recently-used. splice is O(1) and keeps lruIt // (and every other iterator) valid. shapedRunsLru_.splice(shapedRunsLru_.begin(), shapedRunsLru_, it->second.lruIt); } 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 { // Drop matching runs from both the map and the LRU order list, keeping the // two in sync (the list points at keys owned by the map). for (auto it = shapedRuns_.begin(); it != shapedRuns_.end(); ) { if (it->first.font == &font) { shapedRunsLru_.erase(it->second.lruIt); it = shapedRuns_.erase(it); } else { ++it; } } } bool UIRenderer::IsShapedRunCached(const Font& font, float pxSize, std::array color, std::string_view utf8) const { return shapedRuns_.find(ShapedRunViewKey{&font, pxSize, color, utf8}) != shapedRuns_.end(); }