#pragma once #include #include #include #include #include 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. 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). 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(width_), static_cast(height_)}; } /// True while no atlas is on the GPU: there is no text to draw, and `bind()` /// would bind nothing. bool empty() const noexcept { return texture_.id() == 0; } /// 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; }; } // namespace plot::render