2026-05-01 23:35:37 +02:00
|
|
|
|
/*
|
|
|
|
|
|
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;
|
2026-05-18 02:07:48 +02:00
|
|
|
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
2026-05-01 23:35:37 +02:00
|
|
|
|
#include "vulkan/vulkan.h"
|
2026-05-18 04:58:52 +02:00
|
|
|
|
#endif
|
2026-05-02 21:08:20 +02:00
|
|
|
|
export module Crafter.Graphics:FontAtlas;
|
2026-05-01 23:35:37 +02:00
|
|
|
|
import std;
|
|
|
|
|
|
import :Font;
|
2026-05-18 04:58:52 +02:00
|
|
|
|
import :GraphicsTypes;
|
|
|
|
|
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
2026-05-01 23:35:37 +02:00
|
|
|
|
import :ImageVulkan;
|
|
|
|
|
|
import :Device;
|
2026-05-18 04:58:52 +02:00
|
|
|
|
#else
|
|
|
|
|
|
import :WebGPU;
|
|
|
|
|
|
#endif
|
2026-05-01 23:35:37 +02:00
|
|
|
|
|
2026-05-02 21:08:20 +02:00
|
|
|
|
export namespace Crafter {
|
2026-05-01 23:35:37 +02:00
|
|
|
|
// 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 {
|
2026-05-18 04:58:52 +02:00
|
|
|
|
float u0 = 0, v0 = 0;
|
|
|
|
|
|
float u1 = 0, v1 = 0;
|
|
|
|
|
|
float w = 0, h = 0;
|
|
|
|
|
|
float xoff = 0, yoff = 0;
|
|
|
|
|
|
float advance = 0;
|
2026-05-01 23:35:37 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
class FontAtlas {
|
|
|
|
|
|
public:
|
|
|
|
|
|
static constexpr int kAtlasSize = 1024;
|
2026-05-18 04:58:52 +02:00
|
|
|
|
static constexpr float kBaseSize = 32.0f;
|
|
|
|
|
|
static constexpr int kPadding = 4;
|
|
|
|
|
|
static constexpr int kOnEdgeValue = 128;
|
|
|
|
|
|
static constexpr float kPixelDistScale = 32.0f;
|
2026-05-01 23:35:37 +02:00
|
|
|
|
|
2026-06-16 15:40:37 +00:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-18 04:58:52 +02:00
|
|
|
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
2026-05-01 23:35:37 +02:00
|
|
|
|
ImageVulkan<std::uint8_t> image;
|
2026-05-18 04:58:52 +02:00
|
|
|
|
#else
|
|
|
|
|
|
WebGPUTextureRef textureHandle = 0;
|
|
|
|
|
|
std::vector<std::uint8_t> staging;
|
|
|
|
|
|
#endif
|
2026-06-16 15:40:37 +00:00
|
|
|
|
// `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()).
|
2026-05-18 04:58:52 +02:00
|
|
|
|
bool dirty = false;
|
2026-06-16 15:40:37 +00:00
|
|
|
|
DirtyRect dirtyRect;
|
2026-05-01 23:35:37 +02:00
|
|
|
|
|
2026-05-18 04:58:52 +02:00
|
|
|
|
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;
|
2026-05-01 23:35:37 +02:00
|
|
|
|
|
|
|
|
|
|
bool Ensure(Font& font, std::uint32_t codepoint);
|
|
|
|
|
|
const Glyph* Lookup(Font& font, std::uint32_t codepoint) const;
|
2026-05-18 04:58:52 +02:00
|
|
|
|
void Update(GraphicsCommandBuffer cmd);
|
2026-05-01 23:35:37 +02:00
|
|
|
|
|
2026-06-16 15:40:37 +00:00
|
|
|
|
// 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 23:35:37 +02:00
|
|
|
|
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);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|