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

178 lines
7.3 KiB
C++
Raw Normal View History

2026-07-22 18:09:06 +02:00
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
2026-05-18 04:58:52 +02:00
/*
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<float,4> 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);
2026-05-18 04:58:52 +02:00
h._pad = 0;
return h;
}
std::uint32_t UIRenderer::ClipFlags(std::array<float,4> 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<float>(surfaceWidth)
&& clipRectPx[1] + clipRectPx[3] >= static_cast<float>(surfaceHeight);
return clipCoversSurface ? (flags & ~kUIFlagClip) : (flags | kUIFlagClip);
}
2026-05-18 04:58:52 +02:00
std::uint32_t UIRenderer::ShapeText(Font& font, float pxSize,
float x, float baselineY,
std::string_view utf8,
std::array<float,4> color,
GlyphItem* out, std::uint32_t outCapacity,
float* outAdvance,
TextAlign align) {
2026-05-18 04:58:52 +02:00
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; }
2026-05-18 04:58:52 +02:00
const Glyph* g = fontAtlas->EnsureAndGet(font, cp);
if (g == nullptr) continue;
2026-05-18 04:58:52 +02:00
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;
2026-05-18 04:58:52 +02:00
}
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);
2026-05-18 04:58:52 +02:00
}
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;
}
2026-05-18 04:58:52 +02:00
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<float,4> color,
std::string_view utf8) const {
return shapedRuns_.find(ShapedRunViewKey{&font, pxSize, color, utf8})
!= shapedRuns_.end();
}