fix(render): bind identity into the epoch, titles into reserved strips, one layout per frame
Four review findings on the Task 7 renderer: - InstanceStore::epoch() is derived from gl::Buffer::generation() -- advanced by every allocation, deletion and move -- plus the slot the instances were written to, so the orphan fallback of grow_persistent() can no longer leave the vertex array's attribute pointers bound to a deleted buffer: the epoch reports the new identity and upload() re-points them before the draw. - PlotLayout reserves y_title_area/x_title_area outside axes (sized from the text metrics) and draw_titles() draws in them, so a title can no longer land on the data, whatever the envelope does. - draw() computes plot.layout() once and hands it to the helpers, instead of six times per frame, each of which rebuilt the ticks and the grid. - A refused fence is no longer dropped: mark_drawn() falls back to the orphan path and reports the failure rather than rotating through an unsynchronised slot. New smoke cases force both refusals through the GL entry points (glMapBufferRange, glFenceSync) and assert the pixels plus the live transfer mode; the title case asserts the title colour is absent from the bands inside axes and present in both strips. All three fail against the pre-fix sources.
This commit is contained in:
1 parent
3555ab63d7
commit
8f45108559
11 files changed
+492
-70
No files matched your search
@@ -93,6 +93,7 @@ bool Buffer::create(Status* st) noexcept {
|
||||
return fail(st, ErrorCode::GLFailure, "glGenBuffers returned 0");
|
||||
}
|
||||
id_ = name;
|
||||
note_allocated();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -146,6 +147,7 @@ bool VertexArray::create(Status* st) noexcept {
|
||||
return fail(st, ErrorCode::GLFailure, "glGenVertexArrays returned 0");
|
||||
}
|
||||
id_ = name;
|
||||
note_allocated();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -220,6 +222,7 @@ Program Program::from_source(const char* vertex_src, const char* fragment_src,
|
||||
}
|
||||
|
||||
program.id_ = name;
|
||||
program.note_allocated();
|
||||
return program;
|
||||
}
|
||||
|
||||
@@ -285,6 +288,7 @@ bool Texture::create_rgba(u32 width, u32 height, Status* st) noexcept {
|
||||
return fail(st, ErrorCode::GLFailure, "glGenTextures returned 0");
|
||||
}
|
||||
id_ = name;
|
||||
note_allocated();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -304,6 +308,7 @@ bool Texture::create_r8(u32 width, u32 height, Status* st) noexcept {
|
||||
return fail(st, ErrorCode::GLFailure, "glGenTextures returned 0");
|
||||
}
|
||||
id_ = name;
|
||||
note_allocated();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -350,6 +355,7 @@ bool Framebuffer::create(Status* st) noexcept {
|
||||
return fail(st, ErrorCode::GLFailure, "glGenFramebuffers returned 0");
|
||||
}
|
||||
id_ = name;
|
||||
note_allocated();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+46
-4
@@ -310,18 +310,36 @@ const PlotLayout& Plot::layout() {
|
||||
y_width = y_axes_.empty() ? 0.0f : 2.0f * pad;
|
||||
x_height = text_->height() + 2.0f * pad;
|
||||
}
|
||||
// The title strips, though, are needed before the loop and never change, unlike
|
||||
// the label strips: a strip holds exactly one text line, so no tick label can
|
||||
// make it wider. Nothing at all is reserved without a text backend -- there is
|
||||
// then no title to measure, which is what keeps the two defaults above
|
||||
// meaningful for the no-text-backend case. The x strip is one text line tall;
|
||||
// the y strip is one horizontal run wide (see y_title_strip_width).
|
||||
const f32 y_title_w = text_ != nullptr ? y_title_strip_width() : 0.0f;
|
||||
const f32 x_title_h = text_ != nullptr && has_x_title() ? text_->height() + 2.0f * pad : 0.0f;
|
||||
|
||||
for (u32 pass = 0;; ++pass) {
|
||||
// Clamped to the viewport so a huge label set can never push the axes rect
|
||||
// outside it (or make its width negative).
|
||||
// outside it (or make its width negative). The title strips take what is left
|
||||
// after the label strip, so the two can never add up past the viewport.
|
||||
y_width = std::clamp(y_width, 0.0f, viewport_.w);
|
||||
x_height = std::clamp(x_height, 0.0f, viewport_.h);
|
||||
const f32 y_title_width = std::clamp(y_title_w, 0.0f, viewport_.w - y_width);
|
||||
const f32 x_title_height = std::clamp(x_title_h, 0.0f, viewport_.h - x_height);
|
||||
layout_.y_axis_width = y_width;
|
||||
layout_.x_axis_height = x_height;
|
||||
layout_.axes = Rect{viewport_.x + y_width, viewport_.y, viewport_.w - y_width,
|
||||
viewport_.h - x_height};
|
||||
layout_.y_label_area = Rect{viewport_.x, layout_.axes.y, y_width, layout_.axes.h};
|
||||
layout_.y_title_width = y_title_width;
|
||||
layout_.x_title_height = x_title_height;
|
||||
layout_.axes = Rect{viewport_.x + y_width + y_title_width, viewport_.y,
|
||||
viewport_.w - (y_width + y_title_width),
|
||||
viewport_.h - (x_height + x_title_height)};
|
||||
layout_.y_title_area = Rect{viewport_.x, layout_.axes.y, y_title_width, layout_.axes.h};
|
||||
layout_.y_label_area =
|
||||
Rect{layout_.y_title_area.right(), layout_.axes.y, y_width, layout_.axes.h};
|
||||
layout_.x_label_area = Rect{layout_.axes.x, layout_.axes.bottom(), layout_.axes.w, x_height};
|
||||
layout_.x_title_area = Rect{layout_.axes.x, layout_.x_label_area.bottom(), layout_.axes.w,
|
||||
x_title_height};
|
||||
|
||||
refresh_ticks();
|
||||
|
||||
@@ -367,6 +385,30 @@ f64 Plot::snapped_x(const Cursor& c) const {
|
||||
return c.x;
|
||||
}
|
||||
|
||||
bool Plot::has_x_title() const noexcept {
|
||||
for (const Axis& a : x_axes_) {
|
||||
if (!a.label.empty()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Plot::has_y_title() const noexcept {
|
||||
for (const Axis& a : y_axes_) {
|
||||
if (!a.label.empty()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
f32 Plot::y_title_strip_width() const {
|
||||
if (text_ == nullptr) return 0.0f;
|
||||
f32 widest = 0.0f;
|
||||
for (const Axis& a : y_axes_) {
|
||||
if (a.label.empty()) continue;
|
||||
widest = std::max(widest, text_->measure(a.label.c_str()));
|
||||
}
|
||||
return widest > 0.0f ? widest + 2.0f * style_.grid_label_pad : 0.0f;
|
||||
}
|
||||
|
||||
f32 Plot::y_label_width() const {
|
||||
if (text_ == nullptr || y_axes_.empty()) return 0.0f;
|
||||
f32 widest = 0.0f;
|
||||
|
||||
+22
-6
@@ -58,7 +58,10 @@ bool InstanceStore::grow_persistent(usize bytes) noexcept {
|
||||
if (mapped_ == nullptr) {
|
||||
// The driver refused the storage or the mapping. A frame that draws through
|
||||
// the baseline path beats one that draws nothing, so re-create the empty
|
||||
// buffer the orphan path needs and let the next upload use it.
|
||||
// buffer the orphan path needs and let the next upload use it. The vertex
|
||||
// array's attribute pointers went out with the buffer they captured, and the
|
||||
// epoch reports it (`gl::Buffer::generation()`), which is what re-points them
|
||||
// at this buffer before the draw.
|
||||
release_storage();
|
||||
mode_ = UploadMode::Orphan;
|
||||
capacity_ = 0;
|
||||
@@ -68,7 +71,6 @@ bool InstanceStore::grow_persistent(usize bytes) noexcept {
|
||||
}
|
||||
slot_bytes_ = bytes;
|
||||
slot_ = kSlots - 1u; // fresh storage: the first upload takes slot 0
|
||||
++epoch_;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -102,16 +104,30 @@ void InstanceStore::upload(const void* data, u32 count, usize instance_bytes) no
|
||||
}
|
||||
// MAP_COHERENT storage needs no explicit flush, so this is the whole transfer.
|
||||
std::memcpy(mapped_ + usize(slot_) * slot_bytes_, data, bytes);
|
||||
++epoch_;
|
||||
++slot_epoch_;
|
||||
}
|
||||
|
||||
void InstanceStore::mark_drawn() noexcept {
|
||||
bool InstanceStore::mark_drawn() noexcept {
|
||||
if (mode_ != UploadMode::PersistentMapped || mapped_ == nullptr) {
|
||||
return;
|
||||
return true; // nothing reads the store after the draw
|
||||
}
|
||||
Status st;
|
||||
fences_[slot_].reset();
|
||||
(void)fences_[slot_].create(&st);
|
||||
if (fences_[slot_].create(&st)) {
|
||||
return true;
|
||||
}
|
||||
// A slot whose fence the driver refused can never be reused safely: the next
|
||||
// upload would memcpy into storage the GPU may still be reading, which is the
|
||||
// tearing the ring exists to prevent. So the ring is abandoned rather than
|
||||
// rotated through -- the map failure's treatment, one step later -- and the
|
||||
// fresh buffer the orphan path needs is created. Deleting the mapped buffer is
|
||||
// safe while the draw still reads it: GL keeps the storage alive until then.
|
||||
release_storage();
|
||||
mode_ = UploadMode::Orphan;
|
||||
capacity_ = 0;
|
||||
Status retry;
|
||||
(void)buffer_.create(&retry); // a failed retry leaves nothing to draw with
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
+47
-32
@@ -91,10 +91,11 @@ bool Renderer::init(Status* st) noexcept {
|
||||
// best-effort (see api.hpp), so a context can advertise the extension without
|
||||
// the entry points being reachable.
|
||||
const gl::Caps& caps = gl::caps();
|
||||
persistent_ = caps.buffer_storage && caps.persistent_map && gl::glBufferStorage != nullptr &&
|
||||
gl::glMapBufferRange != nullptr && !force_gl33_upload();
|
||||
const bool persistent = caps.buffer_storage && caps.persistent_map &&
|
||||
gl::glBufferStorage != nullptr && gl::glMapBufferRange != nullptr &&
|
||||
!force_gl33_upload();
|
||||
const render::UploadMode mode =
|
||||
persistent_ ? render::UploadMode::PersistentMapped : render::UploadMode::Orphan;
|
||||
persistent ? render::UploadMode::PersistentMapped : render::UploadMode::Orphan;
|
||||
|
||||
font_backend_.reset(font_, style_.axis_font_px);
|
||||
|
||||
@@ -176,17 +177,20 @@ void Renderer::draw(Plot& plot) {
|
||||
// from the values the renderer draws with, so the label pad is never one number
|
||||
// for the layout and another for the text placed inside it.
|
||||
plot.set_style(style_);
|
||||
(void)plot.layout(); // autoscale, ticks and grid for the final axes size
|
||||
// One layout for the whole frame: autoscale, the strip fixed-point loop with
|
||||
// its tick rebuilds, and the grid build all run in here, and every helper
|
||||
// below reads the strips, the axes rect and the transforms it pins.
|
||||
const PlotLayout& layout = plot.layout();
|
||||
|
||||
// Draw order: chrome, then data, then the annotations over it, and the text
|
||||
// last (it is texture-bound, so it is one draw call after every line batch).
|
||||
draw_grid(plot);
|
||||
draw_axis_lines(plot);
|
||||
draw_tick_labels(plot);
|
||||
draw_titles(plot);
|
||||
draw_traces(plot);
|
||||
draw_cursors(plot);
|
||||
draw_measures(plot);
|
||||
draw_tick_labels(plot, layout);
|
||||
draw_titles(plot, layout);
|
||||
draw_traces(plot, layout);
|
||||
draw_cursors(plot, layout);
|
||||
draw_measures(plot, layout);
|
||||
draw_annotations(plot);
|
||||
flush_text();
|
||||
}
|
||||
@@ -241,8 +245,12 @@ void Renderer::flush_lines(const Mapping& map, Color color, f32 width) noexcept
|
||||
gl::glDrawArraysInstanced(gl::GL_TRIANGLE_STRIP, 0, 4,
|
||||
static_cast<gl::GLsizei>(line_vao_.instance_count()));
|
||||
// The draw is in the command stream now, so this is the point at which the
|
||||
// mapped path can let a fence follow it.
|
||||
line_vao_.mark_drawn();
|
||||
// mapped path can let a fence follow it. A driver that refused the fence has
|
||||
// already dropped this store to the baseline path; the frame reports it here
|
||||
// rather than rotating through a slot nothing guards.
|
||||
if (!line_vao_.mark_drawn()) {
|
||||
gl_error_ = true;
|
||||
}
|
||||
stats_.staged_segments += segments;
|
||||
++stats_.draw_calls;
|
||||
}
|
||||
@@ -265,7 +273,9 @@ void Renderer::flush_text() noexcept {
|
||||
text_vao_.bind();
|
||||
gl::glDrawArraysInstanced(gl::GL_TRIANGLE_STRIP, 0, 4,
|
||||
static_cast<gl::GLsizei>(text_vao_.instance_count()));
|
||||
text_vao_.mark_drawn();
|
||||
if (!text_vao_.mark_drawn()) {
|
||||
gl_error_ = true;
|
||||
}
|
||||
++stats_.draw_calls;
|
||||
}
|
||||
glyphs_.clear();
|
||||
@@ -363,8 +373,7 @@ void Renderer::draw_axis_lines(const Plot& plot) {
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::draw_tick_labels(const Plot& plot) {
|
||||
const PlotLayout& layout = plot.layout();
|
||||
void Renderer::draw_tick_labels(const Plot& plot, const PlotLayout& layout) {
|
||||
const f32 line_h = font_backend_.height();
|
||||
|
||||
// X labels: centred under their tick, on the strip's single text line.
|
||||
@@ -397,34 +406,41 @@ void Renderer::draw_tick_labels(const Plot& plot) {
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::draw_titles(const Plot& plot) {
|
||||
// The layout reserves one text line per strip for the tick labels, so an axis
|
||||
// title has no room outside the axes rect. It hugs the inside of its axis line
|
||||
// instead, at the far end of the axis, where it needs no extra layout space and
|
||||
// cannot collide with the labels of the axis it names.
|
||||
const Rect& axes = plot.layout().axes;
|
||||
void Renderer::draw_titles(const Plot& plot, const PlotLayout& layout) {
|
||||
// Every title goes in the strip its direction reserved for it, one text line
|
||||
// outside the tick labels: below the x labels for an x axis, beside the y labels
|
||||
// for a y axis. The strips are what makes a title over the data impossible --
|
||||
// `layout.axes` is inset by them -- rather than merely unlikely, so nothing here
|
||||
// may fall back to an inside-the-axes position.
|
||||
const f32 line_h = font_backend_.height();
|
||||
|
||||
for (u32 i = 0; i < plot.x_axis_count(); ++i) {
|
||||
const Axis& axis = plot.x_axis(i);
|
||||
if (axis.label.empty()) continue;
|
||||
// Centred on the strip: the tick labels have their own row above it, so the
|
||||
// title needs no alignment of its own.
|
||||
const f32 width = font_backend_.measure(axis.label.c_str());
|
||||
const f32 top = axis.place == AxisPlace::Top ? axes.y + 2.0f : axes.bottom() - line_h - 2.0f;
|
||||
stage_text({axes.right() - width - 2.0f, top}, axis.label.c_str());
|
||||
stage_text({layout.x_title_area.x + 0.5f * (layout.x_title_area.w - width),
|
||||
layout.x_title_area.y + 0.5f * (layout.x_title_area.h - line_h)},
|
||||
axis.label.c_str());
|
||||
}
|
||||
for (u32 i = 0; i < plot.y_axis_count(); ++i) {
|
||||
const Axis& axis = plot.y_axis(i);
|
||||
if (axis.label.empty()) continue;
|
||||
// A y title is a horizontal run in the vertical strip the layout sized for
|
||||
// the widest of them, centred on the axes' height: it reads like the x title,
|
||||
// only beside the plot instead of under it. A run longer than the axes is
|
||||
// clipped by the viewport, never folded back over the data.
|
||||
const f32 width = font_backend_.measure(axis.label.c_str());
|
||||
const f32 left = axis.place == AxisPlace::Right ? axes.right() - width - 2.0f : axes.x + 2.0f;
|
||||
stage_text({left, axes.y + 2.0f}, axis.label.c_str());
|
||||
stage_text({layout.y_title_area.x + 0.5f * (layout.y_title_area.w - width),
|
||||
layout.y_title_area.y + 0.5f * (layout.y_title_area.h - line_h)},
|
||||
axis.label.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- data
|
||||
|
||||
void Renderer::draw_traces(const Plot& plot) {
|
||||
const PlotLayout& layout = plot.layout();
|
||||
void Renderer::draw_traces(const Plot& plot, const PlotLayout& layout) {
|
||||
if (!(layout.axes.w > 0.0f)) {
|
||||
return; // a collapsed plot area has no pixel to select a level for
|
||||
}
|
||||
@@ -472,8 +488,7 @@ void Renderer::draw_traces(const Plot& plot) {
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::draw_cursors(const Plot& plot) {
|
||||
const PlotLayout& layout = plot.layout();
|
||||
void Renderer::draw_cursors(const Plot& plot, const PlotLayout& layout) {
|
||||
const Range y_range = plot.y_range(0);
|
||||
const bool log_y = plot.y_axis(0).log;
|
||||
|
||||
@@ -495,8 +510,7 @@ void Renderer::draw_cursors(const Plot& plot) {
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::draw_measures(const Plot& plot) {
|
||||
const PlotLayout& layout = plot.layout();
|
||||
void Renderer::draw_measures(const Plot& plot, const PlotLayout& layout) {
|
||||
const Rect& axes = layout.axes;
|
||||
const Range y_range = plot.y_range(0);
|
||||
const bool log_y = plot.y_axis(0).log;
|
||||
@@ -544,8 +558,9 @@ void Renderer::draw_measures(const Plot& plot) {
|
||||
const f32 limit = std::max(axes.x + 2.0f, axes.right() - width - 2.0f);
|
||||
const f32 left = std::clamp(0.5f * (x0 + x1) - 0.5f * width, axes.x + 2.0f, limit);
|
||||
// The second line of the plot area, not the first: the first belongs to the
|
||||
// y axis title (and to cursor labels), and a wide readout clamped to the left
|
||||
// edge would overprint them.
|
||||
// cursor labels, and a wide readout clamped to the left edge would overprint
|
||||
// them. The y axis title is not a consideration here -- it lives in its own
|
||||
// strip outside the axes.
|
||||
stage_text({left, axes.y + line_h + 2.0f}, readout);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user