Crafter.Graphics/interfaces/Crafter.Graphics-FontAtlas.cppm

154 lines
5.9 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
module;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif
export module Crafter.Graphics:FontAtlas;
import std;
import :Font;
import :GraphicsTypes;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
import :ImageVulkan;
import :Device;
#else
import :WebGPU;
#endif
export namespace Crafter {
// Per-glyph metrics. UVs are 0..1 in atlas space; on-screen sizes /
// offsets / advance are in *atlas pixels at the base size* and scale
// linearly with the requested font size at draw time.
struct Glyph {
float u0 = 0, v0 = 0;
float u1 = 0, v1 = 0;
float w = 0, h = 0;
float xoff = 0, yoff = 0;
float advance = 0;
};
class FontAtlas {
public:
static constexpr int kAtlasSize = 1024;
static constexpr float kBaseSize = 32.0f;
static constexpr int kPadding = 4;
static constexpr int kOnEdgeValue = 128;
static constexpr float kPixelDistScale = 32.0f;
// Bounding box of the atlas pixels touched since the last upload,
// used to copy only the sub-rect that actually changed instead of
// the whole 1 MiB atlas. Half-open: covers [minX, maxX) × [minY,
// maxY). `active` distinguishes "nothing dirty" from a zero-origin
// rect.
struct DirtyRect {
int minX = 0, minY = 0, maxX = 0, maxY = 0;
bool active = false;
bool Empty() const noexcept { return !active; }
void Reset() noexcept { active = false; }
// Grow the box to include [x, x+w) × [y, y+h). No-op for an
// empty rect (a glyph that rasterized to nothing marks nothing).
void Add(int x, int y, int w, int h) noexcept {
if (w <= 0 || h <= 0) return;
if (!active) {
minX = x; minY = y; maxX = x + w; maxY = y + h;
active = true;
} else {
minX = std::min(minX, x);
minY = std::min(minY, y);
maxX = std::max(maxX, x + w);
maxY = std::max(maxY, y + h);
}
}
// Clamp the box to [0, limit] on both axes and emit it as an
// (origin, extent) pair for a GPU copy. Returns false when
// empty, leaving the outputs untouched.
bool UploadBox(int limit, std::uint32_t& x, std::uint32_t& y, std::uint32_t& w, std::uint32_t& h) const noexcept {
if (!active) return false;
const int x0 = std::clamp(minX, 0, limit);
const int y0 = std::clamp(minY, 0, limit);
const int x1 = std::clamp(maxX, 0, limit);
const int y1 = std::clamp(maxY, 0, limit);
x = static_cast<std::uint32_t>(x0);
y = static_cast<std::uint32_t>(y0);
w = static_cast<std::uint32_t>(x1 - x0);
h = static_cast<std::uint32_t>(y1 - y0);
return true;
}
};
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
ImageVulkan<std::uint8_t> image;
#else
WebGPUTextureRef textureHandle = 0;
std::vector<std::uint8_t> staging;
#endif
// `dirty` stays the cheap "is there anything to flush?" flag the
// renderer polls each frame; `dirtyRect` carries the bounds Update
// copies. The two are always set and cleared together (dirty ==
// !dirtyRect.Empty()).
bool dirty = false;
DirtyRect dirtyRect;
void Initialize(GraphicsCommandBuffer cmd);
// Returns the row-major byte pointer the CPU writes pixels into.
// Same shape on both backends.
std::uint8_t* PixelPtr() noexcept;
bool Ensure(Font& font, std::uint32_t codepoint);
// Ensure the glyph is in the atlas and return a pointer to it in one
// hash+probe. Returns nullptr only when a brand-new glyph cannot be
// placed (ShelfPlace failure); a glyph with no SDF coverage (e.g.
// space) still returns a valid pointer.
const Glyph* EnsureAndGet(Font& font, std::uint32_t codepoint);
const Glyph* Lookup(Font& font, std::uint32_t codepoint) const;
void Update(GraphicsCommandBuffer cmd);
// Flag the [x, x+w) × [y, y+h) atlas region as needing re-upload,
// keeping `dirty` in lockstep with the accumulated `dirtyRect`.
void MarkDirty(int x, int y, int w, int h) noexcept {
dirtyRect.Add(x, y, w, h);
dirty = !dirtyRect.Empty();
}
private:
struct Shelf { int y = 0; int height = 0; int cursorX = 0; };
std::vector<Shelf> shelves_;
int nextShelfY_ = 0;
struct Key {
const Font* font;
std::uint32_t cp;
bool operator==(const Key&) const = default;
};
struct KeyHash {
std::size_t operator()(const Key& k) const noexcept {
std::size_t h1 = std::hash<const void*>{}(k.font);
std::size_t h2 = std::hash<std::uint32_t>{}(k.cp);
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
}
};
std::unordered_map<Key, Glyph, KeyHash> cache_;
bool ShelfPlace(int w, int h, int& outX, int& outY);
};
}