// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #include #include #include namespace plot::render { namespace { /// Instance data advances one step per instance, never per vertex: both /// pipelines draw one quad per instance with corners synthesised from /// `gl_VertexID`, so every attribute has a divisor of 1 (core since GL 3.3). constexpr gl::GLuint kPerInstance = 1; } // namespace bool LineVertexArray::create(Status* st, UploadMode mode) noexcept { if (!vao_.create(st)) { return false; } if (!buffer_.create(st, mode)) { vao_.reset(); // do not leave a vertex array behind on the failure path return false; } // Attribute pointers capture the bound GL_ARRAY_BUFFER, so bind both first. vao_.bind(); buffer_.bind(); // Offsets come from the struct, not from counting floats: the layout is the // one shaders.hpp declares, and it stays in step with LineInstance. vao_.attrib(0u, 2, sizeof(LineInstance), offsetof(LineInstance, x0)); vao_.attrib(1u, 2, sizeof(LineInstance), offsetof(LineInstance, x1)); gl::glVertexAttribDivisor(0u, kPerInstance); gl::glVertexAttribDivisor(1u, kPerInstance); gl::glBindVertexArray(0); // leave the binding as we found it epoch_ = buffer_.epoch(); return true; } void LineVertexArray::upload(const f32* interleaved, u32 segments) noexcept { // An array that was never created reports nothing to draw, so a caller that // ignored create()'s status rasterises nothing instead of stale memory. if (interleaved == nullptr || vao_.id() == 0) { instances_ = 0; return; } buffer_.upload(interleaved, segments); // Only the mapped transfer moves the instances between slots, and the // attribute offsets have to follow them. The generation changes for a new slot // or a re-created buffer, never otherwise, so the baseline path re-points at // most once. if (buffer_.epoch() != epoch_) { const usize base = buffer_.slot_offset(); vao_.bind(); buffer_.bind(); vao_.attrib(0u, 2, sizeof(LineInstance), base + offsetof(LineInstance, x0)); vao_.attrib(1u, 2, sizeof(LineInstance), base + offsetof(LineInstance, x1)); epoch_ = buffer_.epoch(); } instances_ = segments; } bool TextVertexArray::create(Status* st, UploadMode mode) noexcept { if (!vao_.create(st)) { return false; } if (!buffer_.create(st, mode)) { vao_.reset(); return false; } vao_.bind(); buffer_.bind(); // Three attributes now: the cell's corner, its atlas rect and the basis that // rotates it (see TextInstance). All of them advance once per instance, like // the line pipeline's, because the corners come from gl_VertexID. vao_.attrib(0u, 2, sizeof(TextInstance), offsetof(TextInstance, x)); vao_.attrib(1u, 2, sizeof(TextInstance), offsetof(TextInstance, u)); vao_.attrib(2u, 2, sizeof(TextInstance), offsetof(TextInstance, ax)); gl::glVertexAttribDivisor(0u, kPerInstance); gl::glVertexAttribDivisor(1u, kPerInstance); gl::glVertexAttribDivisor(2u, kPerInstance); gl::glBindVertexArray(0); epoch_ = buffer_.epoch(); return true; } void TextVertexArray::upload(const f32* interleaved, u32 glyphs) noexcept { if (interleaved == nullptr || vao_.id() == 0) { instances_ = 0; return; } buffer_.upload(interleaved, glyphs); if (buffer_.epoch() != epoch_) { const usize base = buffer_.slot_offset(); vao_.bind(); buffer_.bind(); vao_.attrib(0u, 2, sizeof(TextInstance), base + offsetof(TextInstance, x)); vao_.attrib(1u, 2, sizeof(TextInstance), base + offsetof(TextInstance, u)); vao_.attrib(2u, 2, sizeof(TextInstance), base + offsetof(TextInstance, ax)); epoch_ = buffer_.epoch(); } instances_ = glyphs; } } // namespace plot::render