feat: a repaired invalid span is shaded faintly, not solidly

This commit is contained in:
Martino Ferrari committed 2026-10-08 22:59:07 +02:00
1 parent ebc67bd318
commit 1fc97d818f
7 files changed
+152 -25

No files matched your search

+93 -6
View File
@@ -7241,10 +7241,10 @@ PLOT_TEST(an_invalid_span_is_shaded_and_moves_with_the_data) {
// The bounding box of the pixels the band, and only the band, is responsible for, plus the
// axes rect the frame was laid out into so the caller can check it did not move.
const auto footprint = [&](const plot::Range& view, std::pair<plot::f64, plot::f64> span,
const auto footprint = [&](const plot::Range& view, plot::InvalidSpan span,
plot::Rect& axes_out, const char* dump) {
p.set_x_range(ax, view);
const std::pair<plot::f64, plot::f64> one[1] = {span};
const plot::InvalidSpan one[1] = {span};
p.set_invalid_spans(one);
r.begin_frame(kW, kH, kFrameRect);
r.draw(p);
@@ -7283,7 +7283,7 @@ PLOT_TEST(an_invalid_span_is_shaded_and_moves_with_the_data) {
// A band over the middle third of a 640-wide window.
plot::Rect axes_a{0.0f, 0.0f, 0.0f, 0.0f};
const plot::Rect none_box = footprint({0.0, 640.0}, {640.0 / 3.0, 1280.0 / 3.0}, axes_a,
const plot::Rect none_box = footprint({0.0, 640.0}, {640.0 / 3.0, 1280.0 / 3.0, false}, axes_a,
"invalid_span.ppm");
// Inside the axes on every side -- a hole is not allowed to paint over a label strip -- and
// the full height of the data area, because the whole column has no data.
@@ -7301,8 +7301,8 @@ PLOT_TEST(an_invalid_span_is_shaded_and_moves_with_the_data) {
const plot::f64 per_px = 640.0 / plot::f64(axes_a.w);
const plot::f64 dx = 64.0 * per_px;
plot::Rect axes_b{0.0f, 0.0f, 0.0f, 0.0f};
const plot::Rect panned = footprint({dx, 640.0 + dx}, {640.0 / 3.0, 1280.0 / 3.0}, axes_b,
"invalid_span_panned.ppm");
const plot::Rect panned = footprint({dx, 640.0 + dx}, {640.0 / 3.0, 1280.0 / 3.0, false},
axes_b, "invalid_span_panned.ppm");
PLOT_CHECK_NEAR(panned.x, none_box.x - 64.0f, 2.0);
PLOT_CHECK_NEAR(panned.w, none_box.w, 2.0);
// The layout is identical across both: the band reserved nothing, so the strips the labels
@@ -7340,7 +7340,7 @@ PLOT_TEST(a_sub_pixel_invalid_span_is_still_shaded) {
// One sample's worth of hole in a 200 000-wide window: well under a hundredth of a column.
p.set_x_range(ax, {0.0, 200000.0});
const std::pair<plot::f64, plot::f64> one[1] = {{100000.0, 100000.0}};
const plot::InvalidSpan one[1] = {{100000.0, 100000.0, false}};
p.set_invalid_spans(one);
r.begin_frame(kW, kH, kFrameRect);
r.draw(p);
@@ -7374,3 +7374,90 @@ PLOT_TEST(a_sub_pixel_invalid_span_is_still_shaded) {
std::printf("[invalid span] sub-pixel hole lit %u column(s), %u pixels\n", columns, differing);
ctx.destroy();
}
/// The band carries three states and not two: a hole still missing its samples is shaded solidly,
/// a hole whose late packets have all arrived is shaded faintly, and a hole whose slots have aged
/// out of the host's buffer is not shaded at all. The faint tier is what keeps the report honest
/// -- the window *was* drawn without that data -- while telling a reader it is no longer missing.
///
/// Measured as ink rather than as a sampled pixel: at a fixed view the trace, the grid and the
/// labels are byte-identical across all three frames, so every differing byte is the band's own
/// and a sum of absolute differences measures the tint and nothing else. The alternative -- read
/// one pixel inside the band -- would be measuring whatever the trace happens to do there.
PLOT_TEST(a_repaired_invalid_span_is_fainter_than_a_missing_one) {
plottest::HeadlessContext ctx(kW, kH);
plot::Status st;
PLOT_CHECK(ctx.create(&st));
PLOT_CHECK(plot::gl::load(nullptr));
plot::Renderer r;
PLOT_CHECK(r.init(&st));
plot::data::ArraySeries s(sine_samples(), 0.0, 1.0);
plot::Plot p;
p.set_viewport({0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
const plot::u32 ax = p.add_x_axis({"t", s.x_bounds(), true, plot::AxisPlace::Bottom, false});
const plot::u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
PLOT_CHECK(p.add_trace({&s, plot::Color::hex(0x00E070, 1.0f), 1.5f, ax, ay, "signal"}) == 0u);
p.set_x_range(ax, {0.0, 640.0});
// One band over the middle third, at one fixed view, in each of the three states.
const auto frame = [&](bool shaded, bool repaired, const char* dump) {
const plot::InvalidSpan one[1] = {{640.0 / 3.0, 1280.0 / 3.0, repaired}};
if (shaded) {
p.set_invalid_spans(one);
} else {
p.set_invalid_spans({});
}
r.begin_frame(kW, kH, kFrameRect);
r.draw(p);
r.end_frame();
PLOT_CHECK(!r.last_frame_had_gl_error());
if (dump != nullptr) dump_if_requested(r, dump);
return read_frame(ctx);
};
const std::vector<plot::u8> none = frame(false, false, nullptr);
const std::vector<plot::u8> missing = frame(true, false, "invalid_span_missing.ppm");
const std::vector<plot::u8> repaired = frame(true, true, "invalid_span_repaired.ppm");
// Total ink a frame laid down over the unshaded one, and the columns it covers.
const auto ink = [&](const std::vector<plot::u8>& with, plot::u32& lo_x, plot::u32& hi_x) {
unsigned long long total = 0;
lo_x = kW;
hi_x = 0;
for (plot::u32 y = 0; y < kH; ++y) {
for (plot::u32 x = 0; x < kW; ++x) {
const plot::usize at = (plot::usize(y) * kW + x) * 4u;
unsigned d = 0;
for (plot::usize c = 0; c < 4u; ++c) {
const unsigned a = with[at + c];
const unsigned b = none[at + c];
d += a > b ? a - b : b - a;
}
if (d == 0u) continue;
total += d;
lo_x = std::min(lo_x, x);
hi_x = std::max(hi_x, x);
}
}
return total;
};
plot::u32 m_lo = 0, m_hi = 0, r_lo = 0, r_hi = 0;
const unsigned long long missing_ink = ink(missing, m_lo, m_hi);
const unsigned long long repaired_ink = ink(repaired, r_lo, r_hi);
// A repaired hole is still drawn. A tint that vanished would be the "retire the band on repair"
// behaviour this feature deliberately does not have: the window was missing that data when it
// was drawn, and the band is the record of it.
PLOT_CHECK(repaired_ink > 0ull);
// And the solid one is unmistakably heavier. The defaults are alpha 0.18 against 0.08, better
// than two to one, so half again is a wide margin and not a tuned threshold.
PLOT_CHECK(missing_ink > repaired_ink + repaired_ink / 2ull);
// Same pixels either way: the state chooses the tint and nothing about the geometry.
PLOT_CHECK(m_lo == r_lo && m_hi == r_hi);
std::printf("[invalid span] missing ink %llu over columns %u..%u, repaired ink %llu over "
"%u..%u\n",
missing_ink, m_lo, m_hi, repaired_ink, r_lo, r_hi);
ctx.destroy();
}