fix: a density grid is bounded in cells, not by the window's pixels
A grid was the axes rect in device pixels, so dragging a window wide made the image wider with it -- and the per-frame pass over it is O(cells): the fade multiplies every cell, a host's ink()/peak() read every cell, and a frame the fade moved converts and uploads every cell as one byte. At 2x device scale a 1520x850 window's axes rect is 2877 device pixels wide, so four rows of one trace are 4.5 M cells and 4.9 MB uploaded a frame; measured headless on llvmpipe with a 2 s window, 12.9 ms of tick and 12.8 ms of draw a frame, every frame under a time fade -- a cost that follows the window and has no ceiling. The grid is now the axes rect up to kDensityCellCap (2^18 cells, a mebibyte of counts), scaled down uniformly past it so cells stay square and the picture keeps the axes' aspect, and magnified back over the rect with a linear filter: a cell is a count, so a fragment between two of them is between their two ink levels, which a point sample cannot give once a cell is several pixels wide. The same four rows at the cap are 1.05 M cells, 3.9 ms of tick and 5.6 ms of draw and 1.0 MB uploaded, while a grid under the cap is untouched -- a 640x360 viewport's 573x313 axes is still 179 349 cells, one a device pixel. The image's footprint is the axes rect rounded up to whole device pixels on both sides of the pipe, so a grid under the cap keeps the exact one-texel-a-pixel mapping it has always had (sx/sy come out exactly 1) and only a capped grid reads its cell index through a scale. Moving the decay itself to the GPU was considered and left out: the CPU grid is what ink()/peak() read, what the deposit adds to and what the per-sweep dedupe keys on, so a GPU image would have to be reconciled with it per frame -- a larger change than this bound, and not one to half-do. Tests: a_density_grid_is_capped_whatever_the_window reads the bound off a 3840x2160 viewport (261 504 cells of a 3784x2138 axes, aspect kept), a_capped_density_is_magnified_over_the_axes_rect reads the screen side of it (569x460 cells covering all 832 of the rect's columns), and the density cases now assert their capped figures. 399 cases, 0 failed.
This commit is contained in:
1 parent
1b8b55d3ee
commit
f79a97be9b
12 files changed
+414
-108
No files matched your search
@@ -7725,3 +7725,127 @@ PLOT_TEST(a_phosphor_density_is_the_trace_in_its_own_colour) {
|
||||
bg_g, cell_g, faded_g, steady_g, quarter_g, before, after);
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/// A grid **past `kDensityCellCap`** is the axes rect scaled down, and the frame magnifies the image
|
||||
/// back over that rect: this is the screen-side half of the bound --
|
||||
/// `a_density_grid_is_capped_whatever_the_window` reads the counts of a capped grid, and the two
|
||||
/// agree only if the deposit and the draw use the same footprint, the axes rect in *whole* device
|
||||
/// pixels.
|
||||
///
|
||||
/// What is read here is what a wrong scale moves: the image covers the rect's own columns rather
|
||||
/// than the smaller grid's (the assertion a quad drawn at the grid's size fails), it stays inside
|
||||
/// the rect, and the sweep is on the row its own transform maps y = 0 to, in the trace's colour.
|
||||
PLOT_TEST(a_capped_density_is_magnified_over_the_axes_rect) {
|
||||
const plot::u32 w = 900, h = 720;
|
||||
plottest::HeadlessContext ctx(w, h);
|
||||
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(std::vector<plot::f32>{0.0f, 0.0f}, 0.0, 1.0);
|
||||
plot::Plot p;
|
||||
p.set_viewport({0.0f, 0.0f, static_cast<plot::f32>(w), static_cast<plot::f32>(h)});
|
||||
const plot::u32 ax = p.add_x_axis({"t", {0.0, 1.0}, false, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
const plot::Color colour = plot::Color::hex(0x00E070);
|
||||
PLOT_CHECK(p.add_trace({&s, colour, 1.5f, ax, ay, "flat"}) == 0u);
|
||||
p.set_x_range(ax, {0.0, 1.0});
|
||||
p.set_y_range(ay, {-1.0, 1.0});
|
||||
|
||||
const plot::Rect frame{0.0f, 0.0f, static_cast<plot::f32>(w), static_cast<plot::f32>(h)};
|
||||
r.begin_frame(w, h, frame);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
const std::vector<plot::u8> without = read_frame(ctx);
|
||||
|
||||
plot::DensityConfig cfg;
|
||||
cfg.enabled = true;
|
||||
p.set_density(cfg);
|
||||
p.accumulate_density();
|
||||
const plot::DensityGrid& grid = p.density_grid(0u);
|
||||
const plot::Rect axes = p.layout().axes;
|
||||
// The rect is past the cap, so the grid is smaller than it and the frame has to scale the image
|
||||
// up -- and the one flat sweep still lights every column, once.
|
||||
PLOT_CHECK(grid.cell_count() > 0u);
|
||||
PLOT_CHECK(grid.cell_count() <= plot::kDensityCellCap);
|
||||
PLOT_CHECK(static_cast<plot::u64>(grid.columns()) < static_cast<plot::u64>(std::ceil(axes.w)));
|
||||
PLOT_CHECK(static_cast<plot::u64>(grid.rows()) < static_cast<plot::u64>(std::ceil(axes.h)));
|
||||
PLOT_CHECK_NEAR(grid.ink(), static_cast<double>(grid.columns()), 1.0);
|
||||
|
||||
r.begin_frame(w, h, frame);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
const plot::FrameStats stats = r.stats();
|
||||
PLOT_CHECK(stats.density_cells == static_cast<plot::u32>(grid.cell_count()));
|
||||
PLOT_CHECK(stats.density_upload_bytes == static_cast<plot::u32>(grid.cell_count()));
|
||||
const std::vector<plot::u8> capped = read_frame(ctx);
|
||||
|
||||
const auto differs = [&](plot::u32 x, plot::u32 y) {
|
||||
const plot::usize k = (plot::usize(y) * w + x) * 4u;
|
||||
return capped[k] != without[k] || capped[k + 1u] != without[k + 1u] ||
|
||||
capped[k + 2u] != without[k + 2u];
|
||||
};
|
||||
const auto at = [&](const std::vector<plot::u8>& f, plot::u32 x, plot::u32 y, int c) {
|
||||
return static_cast<int>(f[(plot::usize(y) * w + x) * 4u + static_cast<plot::usize>(c)]);
|
||||
};
|
||||
|
||||
// **The image covers the rect's columns, not the grid's.** A quad drawn at the grid's own size --
|
||||
// the temptation once a cell is no longer a pixel -- would paint the left `columns` of the rect
|
||||
// and leave the rest of it blank, which this counts.
|
||||
plot::u32 changed_cols = 0;
|
||||
plot::u32 lo_x = w, hi_x = 0, lo_y = h, hi_y = 0;
|
||||
for (plot::u32 x = 0; x < w; ++x) {
|
||||
bool any = false;
|
||||
for (plot::u32 y = 0; y < h; ++y) {
|
||||
if (!differs(x, y)) continue;
|
||||
any = true;
|
||||
lo_x = std::min(lo_x, x);
|
||||
hi_x = std::max(hi_x, x);
|
||||
lo_y = std::min(lo_y, y);
|
||||
hi_y = std::max(hi_y, y);
|
||||
}
|
||||
if (any) ++changed_cols;
|
||||
}
|
||||
PLOT_CHECK(changed_cols > 0u);
|
||||
PLOT_CHECK(changed_cols + 2u >= static_cast<plot::u32>(axes.w));
|
||||
// ... and it stays inside the rect it is an image of.
|
||||
PLOT_CHECK(static_cast<plot::f32>(lo_x) >= axes.x - 1.0f);
|
||||
PLOT_CHECK(static_cast<plot::f32>(hi_x) <= axes.right() + 1.0f);
|
||||
PLOT_CHECK(static_cast<plot::f32>(lo_y) >= axes.y - 1.0f);
|
||||
PLOT_CHECK(static_cast<plot::f32>(hi_y) <= axes.bottom() + 1.0f);
|
||||
|
||||
// The sweep is on the row its own transform maps y = 0 to, in the trace's own colour: the cell's
|
||||
// one sweep is spread over the block of device pixels it was magnified into, so what is asserted
|
||||
// is that the ink is *there* -- the brightest row of the rect is that row -- and that its hue is
|
||||
// the trace's rather than the background's.
|
||||
const plot::u32 mid_x = static_cast<plot::u32>(p.x_transform(ax).to_screen_x(0.5));
|
||||
const plot::u32 trace_row = static_cast<plot::u32>(p.y_transform(ay).to_screen_y(0.0));
|
||||
// The background to read the ink against is a row of the *without* frame away from the sweep's
|
||||
// own: the trace itself is drawn there (the density is off in that frame), so the trace's own row
|
||||
// is the trace's colour and not a background at all.
|
||||
const plot::u32 blank_row = trace_row + 40u < h ? trace_row + 40u : trace_row - 40u;
|
||||
const int bg_g = at(without, mid_x, blank_row, 1);
|
||||
int band_g = 0;
|
||||
int band_y = 0;
|
||||
for (plot::u32 y = static_cast<plot::u32>(axes.y); y < static_cast<plot::u32>(axes.bottom()); ++y) {
|
||||
const int g = at(capped, mid_x, y, 1);
|
||||
if (g > band_g) {
|
||||
band_g = g;
|
||||
band_y = static_cast<int>(y);
|
||||
}
|
||||
}
|
||||
PLOT_CHECK(band_g > bg_g + 40); // the sweep is on screen at all
|
||||
PLOT_CHECK(at(capped, mid_x, trace_row, 0) < 40); // ... in the trace's own hue ...
|
||||
PLOT_CHECK(at(capped, mid_x, trace_row, 1) > bg_g + 40);
|
||||
PLOT_CHECK(std::abs(band_y - static_cast<int>(trace_row)) <= 2);
|
||||
|
||||
std::printf("[density gl] capped grid=%ux%u cells=%zu of a %.0fx%.0f axes: changed cols=%u"
|
||||
" brightest row=%d (transform says %u), green=%d against bg=%d\n",
|
||||
grid.columns(), grid.rows(), grid.cell_count(), axes.w, axes.h, changed_cols, band_y,
|
||||
trace_row, band_g, bg_g);
|
||||
ctx.destroy();
|
||||
}
|
||||
Reference in new issue
Block a user