Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/reference/metrics/docgen.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"(appendix lines)": 4335,
"(total)": 2050,
"appendix sections nothing refers to": 192,
"(appendix lines)": 4325,
"(total)": 2038,
"appendix sections nothing refers to": 186,
"member deep dives": 37,
"multi-line comment blocks": 290,
"over-wide comment lines": 718,
"over-wide doc lines": 34,
"split paragraphs": 779
"split paragraphs": 773
}
21 changes: 21 additions & 0 deletions moonlive/effects/basics/ramp.mle
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Ramp: gray rising from black on the left to white on the right, the same on every row, the test pattern for a panel's bit planes.
// Every row matches its neighbors, so a row that differs is a fault in how the panel is driven, not in the picture:
// a HUB75 row lit with the wrong plane's on-time reads brighter than its neighbors below half gray and darker just above it.

class RampEffect {

int dimensions() { return 2; }

string tags() { return "💫"; }

void tick() {
int span = width - 1;
if (span < 1) { span = 1; }
for (int x = 0; x < width; x = x + 1) {
int v = div(x * 255, span);
for (int y = 0; y < height; y = y + 1) {
setRGB(y * width + x, v, v, v);
}
}
}
}
8 changes: 6 additions & 2 deletions src/core/moonlive/script_catalog.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ constexpr const char* kEffectCatalog[] = {
"dot.mle",
"gradient.mle",
"lines.mle",
"ramp.mle",
"random-pixel.mle",
"sparkle.mle",
"mh-aim.mle",
Expand Down Expand Up @@ -81,7 +82,7 @@ constexpr const char* kEffectCatalog[] = {
"stadbeest-thrusters.mle",
"stadbeest-walk.mle",
};
constexpr size_t kEffectCatalogCount = 49;
constexpr size_t kEffectCatalogCount = 50;
/// What each effect above declares about itself, in the same order, 0 meaning it says nothing and the device decides.
constexpr unsigned char kEffectCatalogDim[] = {
1,
Expand All @@ -93,6 +94,7 @@ constexpr unsigned char kEffectCatalogDim[] = {
2,
3,
2,
2,
1,
3,
1,
Expand Down Expand Up @@ -147,6 +149,7 @@ constexpr const char* kEffectCatalogTags[] = {
"💫",
"💫",
"💫",
"💫",
"💫🎯",
"🗼🎶🎯",
"💫🎯",
Expand Down Expand Up @@ -199,6 +202,7 @@ constexpr unsigned char kEffectCatalogFolder[] = {
1,
1,
1,
1,
2,
2,
2,
Expand Down Expand Up @@ -407,6 +411,6 @@ constexpr unsigned char kPaletteCatalogFolder[] = {
10,
};

constexpr size_t kCatalogCount = 77; ///< every factory script, all roles
constexpr size_t kCatalogCount = 78; ///< every factory script, all roles

} // namespace mm::moonlive
6 changes: 5 additions & 1 deletion src/light/drivers/Hub75Slots.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ namespace mm {
///
/// A panel lights where output-enable, the addressed row and the latched row meet, and the latch holds the row last strobed. So row r's words name row r - 1, the latch word moves the address to r in the dark, and row 0's words name the last row.
///
/// The lit window follows the data in the latch: row 0's words light the last row of the plane before, so they take that plane's window, and plane 0's take the top plane's, where the buffer wraps.
///
/// ## Brightness is time
///
/// A panel's chips only switch a column on or off, so plane p lights 2^p as long as plane 0 and brightness shortens every plane's window.
Expand Down Expand Up @@ -71,10 +73,12 @@ inline size_t hub75Encode(const uint8_t* rgb, uint8_t* out,
const uint16_t darkWord = addrMask(latched);
const uint16_t litWord = static_cast<uint16_t>(darkWord | (1u << lay.oe));
const uint16_t latchWord = static_cast<uint16_t>(addrMask(r) | (1u << lay.lat));
// The plane whose data the latch holds while these words clock in, the previous one for row 0: its window is the one these words light.
const uint8_t shownPlane = r == 0 ? static_cast<uint8_t>((plane + geo.bitDepth - 1) % geo.bitDepth) : plane;
// Lit words: the top plane fills the row less its latch, each lower plane half the one above; 32 bits hold 2,048 words at 6 bits.
const uint32_t span = 255u << (geo.bitDepth - 1);
const uint32_t litWords =
(static_cast<uint32_t>(rowWords - 1) * geo.brightness * (1u << plane) + span / 2) / span;
(static_cast<uint32_t>(rowWords - 1) * geo.brightness * (1u << shownPlane) + span / 2) / span;
for (uint16_t pair = 0; pair < pairs; pair++)
for (uint16_t x = 0; x < geo.width; x++) {
// Pair p of step r is row r + p*scanRate, paired half a panel below.
Expand Down
1 change: 1 addition & 0 deletions test/unit/light/moonlive_ir_golden.inc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
{"effects/basics/dot.mle", 0x1e5cd842f29bd16cull},
{"effects/basics/gradient.mle", 0x066b6e80d9fb84cbull},
{"effects/basics/lines.mle", 0x61a519a99d69b4c6ull},
{"effects/basics/ramp.mle", 0x56270ce14f72fa1full},
{"effects/basics/random-pixel.mle", 0xf7fb12703b9df7e6ull},
{"effects/basics/sparkle.mle", 0x6ea5f2562ef2cd4full},
{"effects/moving-heads/mh-aim.mle", 0xa3b9705009ee6730ull},
Expand Down
42 changes: 39 additions & 3 deletions test/unit/light/unit_Hub75Slots.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ uint16_t slot(const std::vector<uint8_t>& out, size_t n) {
return static_cast<uint16_t>(out[n * 2] | (out[n * 2 + 1] << 8));
}

/// The first slot of the row that lights plane p's first row: the one after it latches, since a row is lit while the next one clocks in, and the buffer wraps.
size_t litRowBase(const mm::Hub75Geometry& geo, uint8_t p) {
const size_t rowsInFrame = static_cast<size_t>(geo.bitDepth) * geo.scanRows();
return (static_cast<size_t>(p) * geo.scanRows() + 1) % rowsInFrame * geo.width;
}

} // namespace

TEST_CASE("HUB75 encode: the frame is bit-plane major, one word per column") {
Expand Down Expand Up @@ -206,6 +212,36 @@ TEST_CASE("HUB75 encode: the DMA wrap lights the last row once, from row 0's dat
CHECK((slot(out, 0) & (1u << lay.a)) == (lastRow & 1 ? (1u << lay.a) : 0u));
}

// Seen on two chained 64x64 panels: a line at rows 31 and 63, the last row lit with the next plane's window while the latch held the plane before. White sums the same, so the wrap test above passes either way.
TEST_CASE("HUB75 encode: every row of a value is lit as long as every other, the last row of each half included") {
mm::Hub75Geometry geo;
geo.width = 64; geo.height = 64; geo.scanRate = 32; geo.bitDepth = 6; geo.brightness = 255;
mm::Hub75Layout lay;
std::vector<uint8_t> rgb(static_cast<size_t>(geo.width) * geo.height * 3, 0x7F); // below half: every plane but the top one
std::vector<uint8_t> out(geo.frameBytes(), 0);
REQUIRE(mm::hub75Encode(rgb.data(), out.data(), geo, lay) == geo.frameBytes());

// As the panel sees it: a row lights what the row before it latched, so a row's brightness sums the next row's lit words over the planes where its bit is set.
const uint16_t rows = geo.scanRows();
const size_t rowsInFrame = static_cast<size_t>(geo.bitDepth) * rows;
std::vector<uint32_t> upper(rows, 0), lower(rows, 0);
for (size_t b = 0; b < rowsInFrame; b++) {
const size_t latchedFrom = (b + rowsInFrame - 1) % rowsInFrame;
const uint16_t data = slot(out, latchedFrom * geo.width);
uint32_t lit = 0;
for (uint16_t x = 0; x < geo.width; x++) lit += (slot(out, b * geo.width + x) & (1u << lay.oe)) != 0;
const uint16_t row = static_cast<uint16_t>(latchedFrom % rows);
if (data & (1u << lay.r1)) upper[row] += lit;
if (data & (1u << lay.r2)) lower[row] += lit;
}
REQUIRE(upper[0] > 0);
for (uint16_t r = 0; r < rows; r++) {
CAPTURE(r);
CHECK(upper[r] == upper[0]); // row r of the upper half
CHECK(lower[r] == upper[0]); // row r + 32: the lower half, row 63 the bottom edge
}
}

// Seen on a 64x64 panel: row 63 ghosting onto row 32, rows 0 and 32 brighter, flashing. The peripheral idles its lines LOW between scans, and with OE encoded as the panel reads it (active low) that idle bus lit address 0 with the last latched row.
TEST_CASE("HUB75 encode: the OE bit is set where the panel is lit, so an all-zero idle bus is dark") {
mm::Hub75Geometry geo;
Expand All @@ -215,7 +251,7 @@ TEST_CASE("HUB75 encode: the OE bit is set where the panel is lit, so an all-zer
std::vector<uint8_t> out(geo.frameBytes(), 0);
REQUIRE(mm::hub75Encode(rgb.data(), out.data(), geo, lay) == geo.frameBytes());
// The top plane fills its row, so every column word lights but the latch word.
const size_t top = static_cast<size_t>(geo.bitDepth - 1) * geo.scanRows() * geo.width;
const size_t top = litRowBase(geo, geo.bitDepth - 1);
for (uint16_t x = 0; x + 1 < geo.width; x++) CHECK((slot(out, top + x) & (1u << lay.oe)) != 0);
CHECK((slot(out, top + geo.width - 1) & (1u << lay.oe)) == 0);
}
Expand All @@ -237,9 +273,9 @@ TEST_CASE("HUB75 encode: a row clocks exactly its width, so the last column reac
}

namespace {
// How many words of plane p's first row carry the OE (lit) bit.
// How many words light plane p's first row: the words of the row after it that carry the OE (lit) bit.
size_t litWords(const std::vector<uint8_t>& out, const mm::Hub75Geometry& geo, const mm::Hub75Layout& lay, uint8_t p) {
const size_t base = static_cast<size_t>(p) * geo.scanRows() * geo.width;
const size_t base = litRowBase(geo, p);
size_t n = 0;
for (uint16_t x = 0; x < geo.width; x++) n += (slot(out, base + x) & (1u << lay.oe)) != 0;
return n;
Expand Down
Loading