Make temporal dithering schedules actually work

10/4 planes gives 100fps on the active3 spiral demo on 3 64x64 panels, or
1.2 megapixels/second. And to my eye, there's no brightness shimmer.

All the below settings "look good" to my eye, higher FPS ones tend to look
better to a camera. In "10/4" mode with my camera at 100FPS it still looks
solid but because the beat frequency between the dither pattern and the
shutter is pretty pronounced I can see it shift between the sub-frames.

There's still some brightness variation between modes, with more planes
being a little brighter than fewer planes.

10/0: 50fps
10/2: 72fps
10/4: 100fps

8/0: 92fps
8/2: 109fps
8/4: 134fps

5/0: 135fps
5/2: 153fps
5/4: 210fps
This commit is contained in:
Jeff Epler 2025-03-10 09:42:23 -05:00
parent e2c5bc3467
commit 2442bee476
5 changed files with 67 additions and 30 deletions

View file

@ -34,7 +34,6 @@ def make_pixelmap_multilane(width, height, n_addr_lines, n_lanes):
for lane in range(n_lanes):
y = addr + lane * n_addr
m.append(x + width * y)
print(m)
return m
@ -42,7 +41,7 @@ canvas = Image.new('RGB', (width, height), (0, 0, 0))
draw = ImageDraw.Draw(canvas)
pixelmap = make_pixelmap_multilane(width, height, n_addr_lines, n_lanes)
geometry = piomatter.Geometry(width=width, height=height, n_addr_lines=n_addr_lines, n_planes=8, map=pixelmap, n_lanes=n_lanes)
geometry = piomatter.Geometry(width=width, height=height, n_addr_lines=n_addr_lines, n_planes=10, n_temporal_planes=4, map=pixelmap, n_lanes=n_lanes)
framebuffer = np.asarray(canvas) + 0 # Make a mutable copy
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed,
pinout=piomatter.Pinout.Active3,
@ -122,6 +121,7 @@ try:
draw.circle((x, pen_radius + ((step+1) * (pen_radius* 2) + (2 * (step+1)))), pen_radius, color)
update_matrix()
print(matrix.fps)
clearing = not clearing
except KeyboardInterrupt:

View file

@ -88,19 +88,34 @@ struct schedule_entry {
using schedule = std::vector<schedule_entry>;
using schedule_sequence = std::vector<schedule>;
schedule_sequence rescale_schedule(schedule_sequence ss, size_t pixels_across) {
uint32_t max_active_time = 0;
for (auto &s : ss) {
for (auto &ent : s) {
max_active_time = std::max(ent.active_time, max_active_time);
}
}
if (max_active_time == 0 || max_active_time >= pixels_across) {
return ss;
}
int scale = (pixels_across + max_active_time - 1) / max_active_time;
for (auto &s : ss) {
for (auto &ent : s) {
ent.active_time *= scale;
}
}
return ss;
}
schedule_sequence make_simple_schedule(int n_planes, size_t pixels_across) {
if (n_planes < 1 || n_planes > 10) {
throw std::range_error("n_planes out of range");
}
schedule result;
size_t max_count = 1 << n_planes;
while (max_count < pixels_across)
max_count <<= 1;
for (int i = 0; i < n_planes; i++) {
result.emplace_back(10 - i, max_count >> i);
result.emplace_back(9 - i, (1 << (n_planes - i - 1)));
}
return {result};
return rescale_schedule({result}, pixels_across);
}
// Make a temporal dither schedule. All the top `n_planes` are shown everytime,
@ -117,40 +132,49 @@ schedule_sequence make_temporal_dither_schedule(int n_planes,
return make_simple_schedule(n_planes, pixels_across);
}
if (n_temporal_planes >= n_planes) {
throw std::range_error("n_temporal_planes out of range");
throw std::range_error("n_temporal_planes can't exceed n_planes");
}
if (n_temporal_planes != 2 && n_temporal_planes != 4) {
throw std::range_error("n_temporal_planes out of range");
// the code can generate a schedule for 8 temporal planes, but it
// flickers intolerably
throw std::range_error("n_temporal_planes must be 0, 1, 2, or 4");
}
int n_real_planes = n_planes - n_temporal_planes;
printf("n_planes = %d n_temporal_planes=%d n_real_planes=%d\n", n_planes,
n_temporal_planes, n_real_planes);
uint32_t max_count = 2 << n_real_planes;
uint32_t temporal_count = 1;
while (max_count < pixels_across) {
max_count <<= 1;
temporal_count <<= 1;
}
schedule base_sched;
for (int j = 0; j < n_real_planes; j++) {
base_sched.emplace_back(10 - j, max_count >> j);
base_sched.emplace_back(
9 - j, (1 << (n_temporal_planes + n_real_planes - j - 1)) /
n_temporal_planes);
}
schedule_sequence result;
auto add_sched = [&result, &base_sched](int plane, int count) {
auto sched = base_sched;
sched.emplace_back(10 - plane, count);
sched.emplace_back(9 - plane, count);
result.emplace_back(sched);
};
for (int i = 0; i < n_temporal_planes; i++) {
add_sched(n_real_planes + i, temporal_count << i);
add_sched(n_real_planes + i, 1 << (n_temporal_planes - i - 1));
}
#if 0
std::vector<uint32_t> counts(10, 0);
for (auto s : result) {
for(auto t: s) {
counts[t.shift] += t.active_time;
}
}
for (auto s : counts) {
printf("%d ", s);
}
printf("\n");
#endif
return result;
return rescale_schedule(result, pixels_across);
;
}
struct matrix_geometry {
@ -168,8 +192,8 @@ struct matrix_geometry {
matrix_map map, size_t n_lanes)
: matrix_geometry(pixels_across, n_addr_lines, width, height, map,
n_lanes,
make_temporal_dither_schedule(
n_planes, n_temporal_planes, pixels_across)) {}
make_temporal_dither_schedule(n_planes, pixels_across,
n_temporal_planes)) {}
matrix_geometry(size_t pixels_across, size_t n_addr_lines, size_t width,
size_t height, matrix_map map, size_t n_lanes,

View file

@ -67,9 +67,12 @@ struct piomatter : piomatter_base {
auto &bufseq = buffers[buffer_idx];
bufseq.resize(geometry.schedules.size());
auto converted = converter.convert(framebuffer);
auto old_active_time = geometry.schedules.back().back().active_time;
for (size_t i = 0; i < geometry.schedules.size(); i++) {
protomatter_render_rgb10<pinout>(
bufseq[i], geometry, geometry.schedules[i], converted.data());
protomatter_render_rgb10<pinout>(bufseq[i], geometry,
geometry.schedules[i],
old_active_time, converted.data());
old_active_time = geometry.schedules[i].back().active_time;
}
manager.put_filled_buffer(buffer_idx);
}

View file

@ -132,7 +132,8 @@ struct colorspace_rgb10 {
template <typename pinout>
void protomatter_render_rgb10(std::vector<uint32_t> &result,
const matrix_geometry &matrixmap,
const schedule &sched, const uint32_t *pixels) {
const schedule &sched, uint32_t old_active_time,
const uint32_t *pixels) {
result.clear();
int data_count = 0;
@ -153,7 +154,7 @@ void protomatter_render_rgb10(std::vector<uint32_t> &result,
data_count = n;
};
int32_t active_time;
int32_t active_time = old_active_time;
auto do_data_clk_active = [&active_time, &data_count, &result](uint32_t d) {
bool active = active_time > 0;
@ -193,7 +194,6 @@ void protomatter_render_rgb10(std::vector<uint32_t> &result,
uint32_t addr_bits = calc_addr_bits(prev_addr);
for (size_t addr = 0; addr < n_addr; addr++) {
uint32_t active_time = sched.back().active_time;
for (auto &schedule_ent : sched) {
uint32_t r_mask = 1 << (20 + schedule_ent.shift);
uint32_t g_mask = 1 << (10 + schedule_ent.shift);

View file

@ -124,6 +124,11 @@ int main(int argc, char **argv) {
test_temporal_dither_schedule(5, 1, 2);
test_temporal_dither_schedule(5, 1, 4);
test_simple_dither_schedule(6, 1);
test_temporal_dither_schedule(6, 1, 0);
test_temporal_dither_schedule(6, 1, 2);
test_temporal_dither_schedule(6, 1, 4);
test_simple_dither_schedule(5, 16);
test_temporal_dither_schedule(5, 16, 2);
test_temporal_dither_schedule(5, 16, 4);
@ -132,9 +137,14 @@ int main(int argc, char **argv) {
test_temporal_dither_schedule(5, 24, 2);
test_temporal_dither_schedule(5, 24, 4);
test_simple_dither_schedule(10, 24);
test_temporal_dither_schedule(10, 24, 8);
test_temporal_dither_schedule(5, 128, 4);
test_temporal_dither_schedule(5, 192, 4);
return 0;
piomatter::matrix_geometry geometry(128, 4, 10, 64, 64, true,
piomatter::matrix_geometry geometry(128, 4, 10, 0, 64, 64, true,
piomatter::orientation_normal);
piomatter::piomatter p(std::span(&pixels[0][0], 64 * 64), geometry);