The repository had no licence at all, so every source was all-rights-reserved by default. Add the verbatim MIT text at LICENSE (Copyright (c) 2026 Martino Ferrari) and a two-line SPDX header to each of the 113 first-party sources -- 48 include/plot, 26 src, 10 adapters, 5 examples, 21 tests, 3 tools -- so the grant travels with each file rather than relying on the root document alone. Scripts keep their shebang on line 1; the SPDX lines follow it. Genuinely vendored code is untouched: third_party/stb_truetype.h carries no header of ours. src/render/embedded_mono_font.cpp is generated, so tools/fonts/gen_atlas.py emit() now writes the two lines into its header comment and the file was regenerated -- gen_atlas.py --check passes and the regeneration adds only those comment lines to the previously committed file. README's provenance section now states MIT and links LICENSE. No code, test or public API change.
96 lines
4.4 KiB
C++
96 lines
4.4 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#pragma once
|
|
#include <plot/core/status.hpp>
|
|
#include <plot/core/types.hpp>
|
|
#include <plot/gl/handle.hpp>
|
|
#include <plot/math/vec2.hpp>
|
|
#include <plot/render/text.hpp>
|
|
|
|
namespace plot::render {
|
|
|
|
/// The GPU side of a `TextBackend`'s atlas.
|
|
///
|
|
/// The atlas belongs to the backend, so the renderer does not decide what it
|
|
/// contains or how big it is: `sync()` uploads whatever `atlas_pixels()` points
|
|
/// at, as a single-channel `GL_R8` image of `atlas_width()` by `atlas_height()`,
|
|
/// and re-uploads exactly when the backend's `revision()` changes. Keying that
|
|
/// cache on the revision rather than on a flag set once is what makes a font
|
|
/// swap, or a re-bake at another size, visible -- a cached atlas left in place
|
|
/// draws the previous font's glyphs and nothing in the pixels says why.
|
|
///
|
|
/// It is also where the atlas upload's `GL_UNPACK_ALIGNMENT` excursion is
|
|
/// contained: `Texture::upload_r8` needs 1 for single-channel rows and leaves it
|
|
/// there, so every upload here saves the caller's value and puts it back --
|
|
/// whether it runs in `Renderer::init()` or in the middle of a frame, which is
|
|
/// why the renderer's frame no longer has to carry that value around one.
|
|
///
|
|
/// Nearest filtering, always: the shader reads the texel centre of a glyph cell,
|
|
/// and linear filtering would smear one cell into its neighbour.
|
|
class TextAtlas {
|
|
public:
|
|
TextAtlas() noexcept = default;
|
|
TextAtlas(const TextAtlas&) = delete;
|
|
TextAtlas& operator=(const TextAtlas&) = delete;
|
|
TextAtlas(TextAtlas&&) noexcept = default;
|
|
TextAtlas& operator=(TextAtlas&&) noexcept = default;
|
|
~TextAtlas() = default;
|
|
|
|
/// Brings the texture up to date with `backend`, and does nothing at all when
|
|
/// it already is: the same revision as the last successful upload means the
|
|
/// bytes on the GPU are the ones that revision describes. Any change counts,
|
|
/// including one that moves the revision backwards, so a backend that
|
|
/// re-bakes is never mistaken for one that did not.
|
|
///
|
|
/// A null atlas, or one with a zero width or height, is legal and means "no
|
|
/// text": nothing is uploaded, `empty()` stays true and the resulting draw
|
|
/// stages nothing. The revision is recorded even then, so a backend whose
|
|
/// atlas never appears is not re-checked every frame -- the contract is that a
|
|
/// backend which *starts* providing one bumps its revision.
|
|
///
|
|
/// False, with `st` filled in, when the driver refuses the texture; the last
|
|
/// successful upload is left in place and the next call retries.
|
|
///
|
|
/// It is `ErrorCode::Unsupported` when the GL entry points are not resolved --
|
|
/// a host may hold this class and sync it before `plot::gl::load()`, and no
|
|
/// `gl*` call may be made while the table is unresolved (see
|
|
/// `plot::gl::loader.hpp`) -- reported exactly as every fallible `plot::gl`
|
|
/// handle constructor reports it, rather than called through a null pointer.
|
|
bool sync(const TextBackend& backend, Status* st = nullptr) noexcept;
|
|
|
|
/// Binds to `unit` (texture unit 0 by default, which is where the text
|
|
/// program's sampler points). An empty atlas binds nothing: there is nothing to
|
|
/// sample, and a caller must not draw text from it.
|
|
void bind(gl::GLenum unit = gl::GL_TEXTURE0) const noexcept;
|
|
|
|
/// Atlas size in texels; `(0, 0)` while nothing has been uploaded.
|
|
Vec2 texel_size() const noexcept {
|
|
return Vec2{static_cast<f32>(width_), static_cast<f32>(height_)};
|
|
}
|
|
|
|
/// True while no atlas is on the GPU: there is no text to draw, and `bind()`
|
|
/// binds nothing.
|
|
bool empty() const noexcept { return !has_atlas_; }
|
|
|
|
/// The revision of the atlas currently on the GPU. Meaningless before the
|
|
/// first `sync()`, which is the one call that always uploads.
|
|
u32 revision() const noexcept { return revision_; }
|
|
|
|
private:
|
|
gl::Texture texture_;
|
|
u32 revision_ = 0;
|
|
u32 width_ = 0;
|
|
u32 height_ = 0;
|
|
/// False until a revision has been recorded, so a backend whose `revision()`
|
|
/// is 0 is still uploaded once instead of being mistaken for "already there".
|
|
bool synced_ = false;
|
|
/// False once the backend stops publishing an atlas. It is deliberately not
|
|
/// `texture_.id() != 0`: releasing a GL name needs a resolved entry-point
|
|
/// table, and `empty()` has to keep meaning "there is nothing to draw" even
|
|
/// when a name is still lying around.
|
|
bool has_atlas_ = false;
|
|
};
|
|
|
|
} // namespace plot::render
|