-
-
Notifications
You must be signed in to change notification settings - Fork 9
71 preps #104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
71 preps #104
Conversation
|
Note
|
| Cohort / File(s) | Summary |
|---|---|
Control type size mapping src/MoonBase/Nodes.h, src/MoonBase/Nodes.cpp |
Reordered size-code checks: size 32 now maps to uint32_t, introduced size 33 for int, and shifted float to 34. updateControl casts 32-bit assignments to uint32_t* and adds an int* branch for size 33. |
Monitor matrix dimensions interface/src/routes/moonbase/monitor/monitor.ts |
Added matrixDepth state and changed setMatrixDimensions(width, height, depth = 1); MVP normalization and scaling now include depth. |
Parlio driver API & padding src/MoonLight/Nodes/Drivers/parlio.cpp, src/MoonLight/Nodes/Drivers/parlio.h, src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h |
Major refactor: support per-output leds_per_output (uint16_t*), compute max_leds_per_output, add first_index_per_output and padding, replace isRGBW with components/COMPONENTS_PER_PIXEL, add offsetW, update transposition/packing functions and show_parlio signature and call sites. Widespread logic and signature changes. |
VU meter drawing safety src/MoonLight/Nodes/Effects/E_MoonLight.h |
Clamped VUMeterEffect::drawNeedle end coordinates to layer bounds; minor ternary formatting tweaks. |
Layout default change src/MoonLight/Nodes/Layouts/L_MoonLight.h |
Changed TorontoBarGourdsLayout default granularity from 0 to 2 (per-LED granularity by default). |
Sandbox note src/MoonLight/Nodes/Effects/E__Sandbox.h |
Added comment reminding effects to keep positions within layer bounds; no behavior change. |
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
- Board presets ESP32-P4 #72 — Related changes to Parlio driver signatures, LUT/brightness handling, and leds-per-pin logic.
Poem
🐰 I hopped through bytes and tiny bends,
I nudged the depths and mended ends.
Needles glued inside the frame,
LEDs counted, sized by name.
A little hop — the drivers sing again.
🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 52.63% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. | |
| Title check | ❓ Inconclusive | The title '71 preps' is vague and does not clearly convey the primary changes in this complex multi-system PR affecting crash fixes, monitor rendering, and driver improvements. | Consider a more descriptive title such as 'Fix VUMeter bounds, monitor scaling, and parlio driver for variable LEDs-per-pin' to better summarize the main objectives. |
✅ Passed checks (1 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing touches
- 📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/MoonBase/Nodes.h (1)
158-163: LGTM! Consider using named constants for size codes.The type detection logic is correct and consistent with the corresponding handling in
Nodes.cpp. The use ofstd::is_sameensures exact type matching.Minor suggestion: The magic numbers 33 and 34 for
intandfloat(since they're not actual bit sizes) could be replaced with named constants or an enum for improved readability and maintainability:enum ControlSize : uint8_t { SIZE_UINT8 = 8, SIZE_UINT16 = 16, SIZE_UINT32 = 32, SIZE_INT32 = 33, SIZE_FLOAT = 34 };
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/MoonBase/Nodes.cppsrc/MoonBase/Nodes.h
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (1)
src/MoonBase/Nodes.cpp (1)
34-42: LGTM! Changes are consistent with Nodes.h.The type casting aligns correctly with the size codes defined in
Nodes.h:
- Size 32 →
uint32_t*- Size 33 →
int*- Size 34 →
float*ArduinoJson's implicit conversion handles the value assignment appropriately for these numeric types, including negative values for
int.
|
@coderabbitai, for some reason the VUMeterEffect causes an idle watchdog crash (not encountered with other effects). Can you analyse the crash report: E (66625) task_wdt: Task watchdog got triggered. The following tasks/users did not reset the watchdog in time: Core 0 register dump: Stack memory: ELF file SHA256: 5229cc99a Rebooting... |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 0 🏁 Script executed: Length of output: 4431 🏁 Script executed: Length of output: 1688 🏁 Script executed: Length of output: 10456 🏁 Script executed: Length of output: 1790 @ewowi, I've analyzed the crash report and identified the root cause. This is indeed the same watchdog starvation issue described in the retrieved learning from your previous work on extreme load scenarios. Root Cause AnalysisThe crash occurs because VUMeterEffect's nested loops combined with heavy drawing operations take too long without yielding, starving the IDLE task on CPU 0:
Recommended FixesBased on the retrieved learning, add Primary fix - VUMeterEffect.loop(): void loop() override {
layer->fadeToBlackBy(200);
uint8_t nHorizontal = 4;
uint8_t nVertical = 2;
uint8_t band = 0;
for (int h = 0; h < nHorizontal; h++) {
for (int v = 0; v < nVertical; v++) {
drawNeedle((float)sharedData.bands[2 * (band++)] / 2.0,
{layer->size.x * h / nHorizontal, layer->size.y * v / nVertical, 0},
{layer->size.x / nHorizontal, layer->size.y / nVertical, 0},
ColorFromPalette(layerP.palette, 255 / (nHorizontal * nVertical) * band));
taskYIELD(); // Add yield after each needle draw
}
}
}Defensive fixes - VirtualLayer.cpp: In while (x < y) {
// ... existing setRGB calls ...
x++;
taskYIELD(); // Give IDLE task a chance to run
}In for (uint8_t x = x0; x <= x1; x++) {
// ... existing setRGB calls ...
intersectY += gradient;
if (steep) std::swap(x, y);
taskYIELD(); // Give IDLE task a chance to run
}Would you like me to prepare these code changes for you? 🧠 Learnings used |
|
@coderabbitai, I first start with the VUMeter loop(), I. don't want to add it in drawCircle or drawLine (yet...). tried taskYield first but that gave a crash immediately. I think we concluded earlier that taskYield is not good, if so describe why and add as learning. So I used addYield(10) - the better alternative isn't it? But it also crashed. I will give you the crash report but with a few lines before - maybe that helps analyzing what led to the crash: [ 13496][D][NodeManager.h:87] checkAndAlloc(): [💫] ESP32 SvelteKit (4656) Allocate VU Meter ⏹️ ♫💫📺 Core 0 register dump: Stack memory: ELF file SHA256: 40577d602 Rebooting... |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 7617 @ewowi, I can now explain the issues clearly: Why taskYIELD() Doesn't WorkYes, we already concluded this in a previous interaction (per retrieved learning ID taskYIELD() only yields to equal or higher priority tasks. Since your Effect Task runs at priority 3 and the IDLE task runs at priority 0 (lowest), Why addYield(10) at VirtualLayer Level Doesn't HelpLooking at the code, I see for (auto node : nodes) {
xSemaphoreTake(node->nodeMutex, portMAX_DELAY);
node->loop(); // ← VUMeterEffect.loop() executes here
xSemaphoreGive(node->nodeMutex);
addYield(10); // ← Too late if loop() took >5 seconds
}The problem: If The Real SolutionYou need to add void loop() override {
layer->fadeToBlackBy(200);
uint8_t nHorizontal = 4;
uint8_t nVertical = 2;
uint8_t band = 0;
for (int h = 0; h < nHorizontal; h++) {
for (int v = 0; v < nVertical; v++) {
drawNeedle((float)sharedData.bands[2 * (band++)] / 2.0,
{layer->size.x * h / nHorizontal, layer->size.y * v / nVertical, 0},
{layer->size.x / nHorizontal, layer->size.y / nVertical, 0},
ColorFromPalette(layerP.palette, 255 / (nHorizontal * nVertical) * band));
vTaskDelay(1); // ← Add here, after each drawNeedle
}
}
}This ensures the IDLE task gets CPU time every iteration (8 times per loop), preventing the watchdog timeout. Note: I don't see the ✏️ Learnings added
🧠 Learnings used |
|
@coderabbitai, I am testing on my local drive, not committed yet, but addYield(10) doesn't prevent crashing (which does a vTaskDelay(1) every 10 times, using vTaskDelay(1) directly also doesn't prevent it. One other theory. I removed the bound array checks when calling setLight (without a mapping) as I stated the effect should do it. I remembered the needles of the VUMeter effect can get out of the display, could that cause indirectly an idle error (e.g. if writing to another area e.g. the nr of lights could be changed to something ridiculously high - or something |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 9203 🏁 Script executed: Length of output: 4061 🏁 Script executed: Length of output: 17268 @ewowi, excellent theory! You've found the root cause. The crash is definitely caused by memory corruption from out-of-bounds writes, not CPU starvation. Here's the complete vulnerability chain: The Memory Corruption Chain
Why vTaskDelay(1) Didn't HelpIt's not a scheduling issue—the system is experiencing memory corruption that damages FreeRTOS internals before the watchdog can be properly serviced. Recommended FixesOption 1: Restore bounds checking in setLight (safest): } else {
uint32_t index = indexV * layerP->lights.header.channelsPerLight + offset;
if (index + length <= layerP->lights.maxChannels) { // ✅ Restore this check
memcpy(&layerP->lights.channelsE[index], channels, length);
}
}Option 2: Add defensive bounds checking to drawCircle/drawLine before calling setRGB—check if coordinates are within Option 3: Fix VUMeterEffect to clamp needle coordinates: void drawNeedle(float angle, Coord3D topLeft, Coord3D size, CRGB color) {
int x0 = topLeft.x + size.x / 2;
int y0 = topLeft.y + size.y - 1;
layer->drawCircle(topLeft.x + size.x / 2, topLeft.y + size.y / 2, size.x / 2,
ColorFromPalette(layerP.palette, 35, 128), false);
int x1 = x0 - round(size.y * 0.7 * cos((angle + 30) * PI / 180));
int y1 = y0 - round(size.y * 0.7 * sin((angle + 30) * PI / 180));
// ✅ Clamp to valid bounds
x1 = max(0, min(x1, (int)layer->size.x - 1));
y1 = max(0, min(y1, (int)layer->size.y - 1));
layer->drawLine(x0, y0, x1, y1, color, true);
}I recommend Option 1 (restore bounds check) because it prevents all effects from causing memory corruption, not just VUMeter. Your original learnings about removing bounds checks for performance (IDs 9d55622b, fb7705fb) apply to the mapped path, but the unmapped path should retain safety checks since it's writing directly to the array based on calculated indices. Would you like me to prepare these changes? 🧠 Learnings used |
|
@coderabbitai, regarding
This solved the problem, cool. One more thing, the monitor is not aligned in the centre, it is slight shifted right, causing the right part to fall off the view window. Please come with a fix |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 3362 @ewowi, Great that the clamping fixed the crash! The alignment issue is caused by integer division losing remainder pixels, making the grid left-aligned instead of centered. The ProblemWhen
The FixCalculate an offset to center the grid within the display: void loop() override {
layer->fadeToBlackBy(200);
uint8_t nHorizontal = 4;
uint8_t nVertical = 2;
// Calculate cell dimensions
uint16_t cellWidth = layer->size.x / nHorizontal;
uint16_t cellHeight = layer->size.y / nVertical;
// Calculate offset to center the grid
uint16_t offsetX = (layer->size.x - cellWidth * nHorizontal) / 2;
uint16_t offsetY = (layer->size.y - cellHeight * nVertical) / 2;
uint8_t band = 0;
for (int h = 0; h < nHorizontal; h++) {
for (int v = 0; v < nVertical; v++) {
drawNeedle(
(float)sharedData.bands[2 * (band++)] / 2.0,
{offsetX + cellWidth * h, offsetY + cellHeight * v, 0}, // Centered topLeft
{cellWidth, cellHeight, 0}, // Cell size
ColorFromPalette(layerP.palette, 255 / (nHorizontal * nVertical) * band)
);
}
}
}This ensures the 4×2 grid is perfectly centered regardless of display dimensions, preventing any part from falling off the edge. 🧠 Learnings used |
|
@coderabbitai, Regarding
It's related to Monitor.svelte and Monitor.ts, it happens with any effect displayed |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain@ewowi, you're absolutely right - I apologize for the confusion. The monitor alignment issue is in the UI components, not the effect code. Let me investigate the Monitor.svelte and Monitor.ts files. 🏁 Script executed: Length of output: 5056 🏁 Script executed: Length of output: 6103 @ewowi, I found the issue! The problem is in the The Root CauseThe code calculates separate distances for width and height fit, then uses the maximum with padding. However, when one dimension requires more distance than the other, the smaller dimension isn't centered—it's just scaled and positioned at the origin without considering that it should be offset to appear centered in the viewport. The FixModify the function getMVPMatrix(): mat4 {
const canvas = gl!.canvas as HTMLCanvasElement;
const canvasAspect = canvas.width / canvas.height;
const fov = Math.PI / 6; // 30 degrees
const near = 0.1;
const far = 100.0;
const projection = mat4.create();
mat4.perspective(projection, fov, canvasAspect, near, far);
// Normalize the matrix dimensions to a unit square/rectangle
const maxDim = Math.max(matrixWidth, matrixHeight);
const normalizedWidth = matrixWidth / maxDim;
const normalizedHeight = matrixHeight / maxDim;
// Calculate LED matrix aspect ratio
const matrixAspect = matrixWidth / matrixHeight;
// Calculate scale factor to fit the matrix in the view while maintaining aspect ratio
let scale = 1.0;
if (matrixAspect > canvasAspect) {
// Matrix is wider than canvas - scale to fit width
scale = normalizedWidth;
} else {
// Matrix is taller than canvas - scale to fit height
scale = normalizedHeight / canvasAspect;
}
// Calculate camera distance to fit the scaled matrix
const effectiveSize = scale;
const cameraDistance = effectiveSize / (2 * Math.tan(fov / 2)) * 1.5; // 1.5 for padding
const view = mat4.create();
mat4.lookAt(view, [0, 0, cameraDistance], [0, 0, 0], [0, 1, 0]);
const model = mat4.create();
// Scale uniformly by the effective size to maintain aspect ratio and centering
mat4.scale(model, model, [normalizedWidth, normalizedHeight, 1]);
const mvp = mat4.create();
mat4.multiply(mvp, projection, view);
mat4.multiply(mvp, mvp, model);
return mvp;
}This approach:
The issue was that the previous code calculated distances for both width and height independently then took the max, which doesn't guarantee proper centering when aspects don't match. 🧠 Learnings used |
|
@coderabbitai, regarding
You proposed const cameraDistance = effectiveSize / (2 * Math.tan(fov / 2)) * 1.5; // 1.5 for padding But the effect was bigger then the display so had to change to const cameraDistance = effectiveSize / (2 * Math.tan(fov / 2)) * 5; // 5 for padding Could I use 5 instead of 1.5 or do I get off displays under certain circumstances? Can you also explain why 5 works and 1.5 not? |
|
Tip For best results, initiate chat on the files or code changes. @ewowi, yes, using 5 is perfectly safe and won't cause clipping issues under any circumstances. Here's why: Why 5 Works But 1.5 Doesn'tThe padding multiplier controls how far the camera moves away from the model:
The base formula
The reason 1.5 didn't work: My Why 5 works: It compensates for this underestimation by moving the camera much farther back, ensuring adequate margin around the LED matrix regardless of aspect ratio. Is 5 Safe?Absolutely. Using a larger padding factor like 5:
If you want less margin (bigger LED matrix), you could experiment with values like 3 or 4, but 5 is a safe, conservative choice that guarantees the entire matrix is always visible with comfortable padding. ✏️ Learnings added
🧠 Learnings used |
|
@coderabbitai, regarding
I found out with 5: 128 x 128 works okay 64 x 128 works okay, 128x 64 works okay, but 128 x 32 not (too small). This problem was not in the current solution, it was only shifted right. So can you look back at the original code and combine this with your solution so it scales well AND it aligns well |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 4133 🏁 Script executed: Length of output: 6173 🏁 Script executed: Length of output: 2917 @ewowi, I found the issue! The problem is that vertices are already normalized to [-1, 1] in Monitor.svelte, then they're being scaled again in The Root CauseIn Monitor.svelte (vertex generation): x = width == 1 ? 0 : (x / (width - 1)) * 2.0 - 1.0;
y = height == 1 ? 0 : ((height - 1 - y) / (height - 1)) * 2.0 - 1.0;Vertices are normalized to [-1, 1] range and centered at origin. Then in monitor.ts (getMVPMatrix): mat4.scale(model, model, [normalizedWidth, normalizedHeight, 1]);They're scaled again by aspect ratio factors. The original camera distance calculation The FixKeep the original scaling and camera distance logic (it works!), but add an explicit translation to center the model properly: function getMVPMatrix(): mat4 {
const canvas = gl!.canvas as HTMLCanvasElement;
const canvasAspect = canvas.width / canvas.height;
const fov = Math.PI / 6; // 30 degrees
const near = 0.1;
const far = 100.0;
const projection = mat4.create();
mat4.perspective(projection, fov, canvasAspect, near, far);
// Normalize the matrix dimensions to a unit square/rectangle
const maxDim = Math.max(matrixWidth, matrixHeight);
const normalizedWidth = matrixWidth / maxDim;
const normalizedHeight = matrixHeight / maxDim;
// Calculate required distance for vertical fit
const verticalSize = normalizedHeight;
const distanceForHeight = verticalSize / (2 * Math.tan(fov / 2));
// Calculate required distance for horizontal fit
const horizontalFov = 2 * Math.atan(Math.tan(fov / 2) * canvasAspect);
const horizontalSize = normalizedWidth;
const distanceForWidth = horizontalSize / (2 * Math.tan(horizontalFov / 2));
// Use the larger distance to ensure both dimensions fit (keeps original scaling logic)
const cameraDistance = Math.max(distanceForHeight, distanceForWidth) * 2.5;
const view = mat4.create();
mat4.lookAt(view, [0, 0, cameraDistance], [0, 0, 0], [0, 1, 0]);
const model = mat4.create();
// Center the model at origin BEFORE scaling
mat4.translate(model, model, [-0.5 * (normalizedWidth - 1), -0.5 * (normalizedHeight - 1), 0]);
// Scale by the normalized dimensions
mat4.scale(model, model, [normalizedWidth, normalizedHeight, 1]);
const mvp = mat4.create();
mat4.multiply(mvp, projection, view);
mat4.multiply(mvp, mvp, model);
return mvp;
}The key addition is: mat4.translate(model, model, [-0.5 * (normalizedWidth - 1), -0.5 * (normalizedHeight - 1), 0]);This compensates for the asymmetry introduced by scaling the pre-normalized vertices, ensuring the scaled model remains centered at the origin before the camera views it. This should fix the right-shift while maintaining proper scaling for all aspect ratios including 128x32. 🧠 Learnings used |
|
@coderabbitai, regarding
This looks better, however it's not vertically centered anymore, please fix |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 2496 @ewowi, I apologize - my translation formula was incorrect. Let me fix this properly. The Problem with My Previous FixThe translation formula I provided was mathematically wrong and would cause unpredictable shifts. Here's why: Vertices in The Correct FixRemove the translation entirely - the scaling alone should work: function getMVPMatrix(): mat4 {
const canvas = gl!.canvas as HTMLCanvasElement;
const canvasAspect = canvas.width / canvas.height;
const fov = Math.PI / 6; // 30 degrees
const near = 0.1;
const far = 100.0;
const projection = mat4.create();
mat4.perspective(projection, fov, canvasAspect, near, far);
// Normalize the matrix dimensions to a unit square/rectangle
const maxDim = Math.max(matrixWidth, matrixHeight);
const normalizedWidth = matrixWidth / maxDim;
const normalizedHeight = matrixHeight / maxDim;
// Calculate required distance for vertical fit
const verticalSize = normalizedHeight;
const distanceForHeight = verticalSize / (2 * Math.tan(fov / 2));
// Calculate required distance for horizontal fit
const horizontalFov = 2 * Math.atan(Math.tan(fov / 2) * canvasAspect);
const horizontalSize = normalizedWidth;
const distanceForWidth = horizontalSize / (2 * Math.tan(horizontalFov / 2));
// Use the larger distance to ensure both dimensions fit
const cameraDistance = Math.max(distanceForHeight, distanceForWidth) * 2.5;
const view = mat4.create();
mat4.lookAt(view, [0, 0, cameraDistance], [0, 0, 0], [0, 1, 0]);
const model = mat4.create();
// Scale by normalized dimensions - this preserves centering since vertices are pre-centered
mat4.scale(model, model, [normalizedWidth, normalizedHeight, 1]);
const mvp = mat4.create();
mat4.multiply(mvp, projection, view);
mat4.multiply(mvp, mvp, model);
return mvp;
}This is essentially the current code. If you're still seeing a right shift with this code, the issue is not in
Can you confirm whether the right shift occurs with the code exactly as shown above (no translation)? If so, we need to investigate other parts of the rendering pipeline. 🧠 Learnings used |
|
@coderabbitai, that's better, it only looks that 3D objects are slightly bigger than 2D objects, I can correct it by setting the cameradistance multiplier to 3 instead of 2.5, but then 2D is too small. Please find out why and correct |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 8641 @ewowi, I found the issue! The problem is that 3D objects have z-coordinates ranging from -1 to 1, while 2D objects have all vertices at z=0. However, the model matrix only normalizes x and y dimensions—it always scales z by 1. The Root CauseIn Monitor.svelte (line 121):
In monitor.ts getMVPMatrix() (line 185): mat4.scale(model, model, [normalizedWidth, normalizedHeight, 1]); // ❌ z always scaled by 1Since z isn't normalized, 3D objects have vertices spread out in depth, with some closer to the camera. In perspective projection, closer vertices appear larger, making the entire 3D object look bigger overall compared to 2D objects (all at z=0). The FixTrack and normalize the depth dimension just like width and height: // Store LED matrix dimensions
let matrixWidth: number = 1;
let matrixHeight: number = 1;
let matrixDepth: number = 1; // ✅ Add depth tracking
// ... existing code ...
// New function to set the LED matrix dimensions
export function setMatrixDimensions(width: number, height: number, depth: number = 1) { // ✅ Add depth parameter
matrixWidth = width;
matrixHeight = height;
matrixDepth = depth;
}
function getMVPMatrix(): mat4 {
const canvas = gl!.canvas as HTMLCanvasElement;
const canvasAspect = canvas.width / canvas.height;
const fov = Math.PI / 6; // 30 degrees
const near = 0.1;
const far = 100.0;
const projection = mat4.create();
mat4.perspective(projection, fov, canvasAspect, near, far);
// Normalize ALL dimensions including depth
const maxDim = Math.max(matrixWidth, matrixHeight, matrixDepth); // ✅ Include depth in max
const normalizedWidth = matrixWidth / maxDim;
const normalizedHeight = matrixHeight / maxDim;
const normalizedDepth = matrixDepth / maxDim; // ✅ Normalize depth
// Calculate required distance for vertical fit
const verticalSize = normalizedHeight;
const distanceForHeight = verticalSize / (2 * Math.tan(fov / 2));
// Calculate required distance for horizontal fit
const horizontalFov = 2 * Math.atan(Math.tan(fov / 2) * canvasAspect);
const horizontalSize = normalizedWidth;
const distanceForWidth = horizontalSize / (2 * Math.tan(horizontalFov / 2));
// Use the larger distance to ensure both dimensions fit
const cameraDistance = Math.max(distanceForHeight, distanceForWidth) * 2.5;
const view = mat4.create();
mat4.lookAt(view, [0, 0, cameraDistance], [0, 0, 0], [0, 1, 0]);
const model = mat4.create();
// Scale by ALL normalized dimensions ✅
mat4.scale(model, model, [normalizedWidth, normalizedHeight, normalizedDepth]);
const mvp = mat4.create();
mat4.multiply(mvp, projection, view);
mat4.multiply(mvp, mvp, model);
return mvp;
}And update the call in Monitor.svelte (line 93): setMatrixDimensions(width, height, depth); // ✅ Pass depthThis ensures 2D and 3D objects are scaled consistently, making them appear the same size when they have the same width×height dimensions. 🧠 Learnings used |
frontend ======= - Monitor: fix scaling and aligning backend ======= - MoonLight Effects: fix VUMeter
instead of the first length First step to see if we can deal with different nr of leds per pin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h:
- Around line 64-67: The loop computing maxLedsPerPin incorrectly iterates the
whole ledsPerPin array and can pick up UINT16_MAX sentinels; change it to
iterate only the first layerP.nrOfPins entries (use an index-based loop over
0..nrOfPins-1) when scanning layerP.ledsPerPin so only initialized pin counts
are considered; update references to maxLedsPerPin, layerP.ledsPerPin, and
layerP.nrOfPins accordingly.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
lib/framework/WWWData.hsrc/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 84
File: src/MoonLight/Modules/ModuleLightsControl.h:367-379
Timestamp: 2025-12-28T10:28:24.157Z
Learning: In MoonLight PR #84: wrapping effectTask's layerP.loop() with monitorMutex while reading channelsE for monitor emission is thread-safe and prevents partial frames, but introduces serialization between effects rendering and monitor emission. Reading from channelsD (driver/front buffer) instead—with monitorMutex held only during the brief pointer swap—is superior because it maintains full parallelism (effects ║ monitor ║ driver), shows actual LED output, and minimizes mutex hold time.
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-06T13:45:03.067Z
Learning: For MoonLight on ESP32 at extreme loads (e.g., 49K channels @ 100 fps): Task watchdog can fire when AppDrivers/AppEffects process many nodes without yielding. Even with vTaskDelay(1) at loop end, the node iteration for-loops (PhysicalLayer::loopDrivers line 108-111, PhysicalLayer::loop line 73-75) can take >5 seconds without yielding, starving IDLE task (priority 0). Solution: add taskYIELD() or vTaskDelay(1) inside the node/layer iteration loops to give IDLE task chances to run and reset watchdog. Lowering task priority won't help since IDLE is already at 0 (lowest).
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-12T17:15:32.074Z
Learning: In MoonLight's Monitor.ts (interface/src/routes/moonbase/monitor/monitor.ts), the camera distance calculation uses a padding multiplier in the formula: `cameraDistance = effectiveSize / (2 * Math.tan(fov / 2)) * padding`. A padding value of 5 (not 1.5) is needed because the effectiveSize calculation underestimates the actual visible model extent after scaling by [normalizedWidth, normalizedHeight, 1]. Larger padding values move the camera farther away, making the object smaller with more margin. Using 5 is safe and prevents clipping for all LED matrix aspect ratios.
📚 Learning: 2026-01-06T15:24:29.770Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.
Applied to files:
src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h
📚 Learning: 2026-01-06T15:24:17.118Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:164-171
Timestamp: 2026-01-06T15:24:17.118Z
Learning: In VirtualLayer::setLight and VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking in the unmapped path (when indexV >= mappingTableSize or oneToOneMapping is true) is intentionally disabled for performance when driving very large LED counts (100K+). Effect developers are responsible for ensuring their code stays within bounds. This is an explicit design decision prioritizing performance over defensive checks in the hot path.
Applied to files:
src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h
📚 Learning: 2025-12-28T10:28:24.157Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 84
File: src/MoonLight/Modules/ModuleLightsControl.h:367-379
Timestamp: 2025-12-28T10:28:24.157Z
Learning: In MoonLight PR #84: wrapping effectTask's layerP.loop() with monitorMutex while reading channelsE for monitor emission is thread-safe and prevents partial frames, but introduces serialization between effects rendering and monitor emission. Reading from channelsD (driver/front buffer) instead—with monitorMutex held only during the brief pointer swap—is superior because it maintains full parallelism (effects ║ monitor ║ driver), shows actual LED output, and minimizes mutex hold time.
Applied to files:
src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h
📚 Learning: 2025-12-07T14:16:03.228Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-07T14:16:03.228Z
Learning: VirtualLayer mappingTableSize is uint16_t with a maximum of 65535 LEDs. If a user defines a space where size.x * size.y * size.z exceeds 65535, the code accepts the overflow and performs mapping partially. This is an explicit design decision balancing performance and memory constraints.
Applied to files:
src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h
🧬 Code graph analysis (1)
src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h (1)
src/MoonLight/Nodes/Drivers/parlio.cpp (1)
show_parlio(267-267)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (1)
src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h (1)
70-70: LGTM - correct semantic change for buffer sizing.Passing
maxLedsPerPininstead of the first pin's LED count ensures the Parlio driver allocates sufficient buffer for the largest output channel, which is the correct approach for parallel LED driving.
|
@coderabbitai, see issue #95 : A limitation of the parallel LED driver for esp32-p4 is that it all the leds_per_output / leds per pin should be the same for each pin. I try to find a solution to deal with setups with different leds per pin per pin. A necessary but probably not sufficient change is that the call to show_parlio() should set the argument leds_per_output to the max instead of only layerP.ledsPerPin[0] as I had before. Next step is that the implementation of show_parlio() should sent maxLedsPerPin to each pin, as the real world does not care if there are physical leds connected or not to a pin. So there will be a number of leds connected <= maxLedsPerPin on each pin. So my idea is that the implementation of show_parlio() will do that and it can use the layerP.lights.channelsD array it gets as input parameter but if for a specific pin maxLedsPerPin is larger it should send dummy data to that pin. See the file parlio.cpp for the implementation. You see there show_parlio() calls create_transposed_led_output_optimized() using parallel_buffer_remapped and parallel_buffer_repacked which is related to buffer_in which is related to my layerP.lights.channelsD (which contains just the real number of leds per pin, not maxLedsPerPin. So my question is: can you propose code changes such that show_parlio() sends maxLedsPerPin per pin, taking whatever it does with layerP.lights.channelsD but filling up the space with dummy values until maxLedsPerPin |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 23403 @ewowi, I've analyzed the code and understand the issue. The problem is that Proposed SolutionModify void loop() override {
#ifdef ESP32_LEDSDRIVER
DriverNode::loop();
#ifndef CONFIG_IDF_TARGET_ESP32P4
if (ledsDriver.total_leds > 0) ledsDriver.showPixels(WAIT);
#else
uint8_t nrOfPins = min(layerP.nrOfLedPins, layerP.nrOfAssignedPins);
// Get max leds per pin
uint16_t maxLedsPerPin = 0;
for (uint16_t ledsPerPin : layerP.ledsPerPin) {
if (ledsPerPin > maxLedsPerPin) maxLedsPerPin = ledsPerPin;
}
// Calculate padded buffer size
uint8_t channelsPerPixel = layerP.lights.header.channelsPerLight;
uint32_t paddedLength = nrOfPins * maxLedsPerPin;
uint32_t paddedBufferSize = paddedLength * channelsPerPixel;
// Allocate padded buffer (static to avoid repeated allocations)
static uint8_t* paddedBuffer = nullptr;
static uint32_t lastPaddedBufferSize = 0;
if (paddedBuffer == nullptr || paddedBufferSize != lastPaddedBufferSize) {
if (paddedBuffer != nullptr) {
heap_caps_free(paddedBuffer);
}
// Allocate DMA-capable memory
paddedBuffer = (uint8_t*)heap_caps_malloc(paddedBufferSize, MALLOC_CAP_DMA);
lastPaddedBufferSize = paddedBufferSize;
if (paddedBuffer == nullptr) {
EXT_LOGE(ML_TAG, "Failed to allocate padded buffer for parallel output");
return;
}
}
// Zero out the entire buffer (dummy LEDs will be black)
memset(paddedBuffer, 0, paddedBufferSize);
// Copy actual LED data for each pin with padding
uint32_t srcOffset = 0; // Current position in channelsD
uint32_t dstOffset = 0; // Current position in paddedBuffer
for (uint8_t pin = 0; pin < nrOfPins; pin++) {
uint16_t actualLeds = layerP.ledsPerPin[pin];
uint32_t actualBytes = actualLeds * channelsPerPixel;
// Copy actual LED data for this pin
memcpy(paddedBuffer + dstOffset, layerP.lights.channelsD + srcOffset, actualBytes);
srcOffset += actualBytes;
dstOffset += maxLedsPerPin * channelsPerPixel; // Move to next pin's slot (includes padding)
}
// Call show_parlio with padded buffer
show_parlio(pins, paddedLength, paddedBuffer,
channelsPerPixel == 4, nrOfPins, maxLedsPerPin,
layerP.lights.header.offsetRed,
layerP.lights.header.offsetGreen,
layerP.lights.header.offsetBlue);
#endif
#else // ESP32_LEDSDRIVER
// ... existing code ...
#endif
}How It Works
Result
This approach keeps the complexity in the driver layer and doesn't require changes to the low-level 🧠 Learnings used |
|
@coderabbitai, your solution creates another padded buffer which I think is not optimal, could you come with an alternative solution where the padding is done within parlio.cpp where the transpose is taking place so we just use the buffers created there and pad these buffers |
|
Tip For best results, initiate chat on the files or code changes. @ewowi, excellent point! Moving the padding into Modified parlio.cppChange extern "C" void show_parlio(
const unsigned char *pins,
unsigned int length, // Now: sum of actual LEDs across all pins
const unsigned char *buffer_in,
bool has_white,
unsigned int outputs,
unsigned int leds_per_output, // Now: maxLedsPerPin
const uint16_t *actual_leds_per_pin, // ✅ NEW: array of actual LED counts per pin
unsigned int offsetRed,
unsigned int offsetGreen,
unsigned int offsetBlue
)
{
// ... existing setup code ...
unsigned int channels_per_led = has_white ? 4 : 3;
unsigned int total_leds = outputs * leds_per_output; // Padded total
unsigned int total_channels = total_leds * channels_per_led;
// Allocate buffers for padded size
unsigned int total_transposed_size = leds_per_output * outputs * channels_per_led;
if (parallel_buffer_remapped == NULL || last_total_transposed_size != total_transposed_size) {
// ... existing reallocation logic ...
}
// ✅ NEW: Remap with per-pin padding
unsigned int src_offset = 0; // Position in buffer_in (actual data)
unsigned int dst_offset = 0; // Position in parallel_buffer_remapped (padded)
for (unsigned int pin = 0; pin < outputs; pin++) {
uint16_t actual_leds = actual_leds_per_pin[pin];
// Copy actual LEDs for this pin
for (unsigned int led = 0; led < actual_leds; led++) {
unsigned int src_base = src_offset + (led * channels_per_led);
unsigned int dst_base = dst_offset + (led * channels_per_led);
parallel_buffer_remapped[dst_base + 0] = buffer_in[src_base + offsetRed];
parallel_buffer_remapped[dst_base + 1] = buffer_in[src_base + offsetGreen];
parallel_buffer_remapped[dst_base + 2] = buffer_in[src_base + offsetBlue];
if (has_white && channels_per_led == 4) {
parallel_buffer_remapped[dst_base + 3] = buffer_in[src_base + 3];
}
}
// Pad remaining LEDs with zeros (black)
unsigned int padding_leds = leds_per_output - actual_leds;
unsigned int padding_start = dst_offset + (actual_leds * channels_per_led);
unsigned int padding_bytes = padding_leds * channels_per_led;
memset(parallel_buffer_remapped + padding_start, 0, padding_bytes);
// Advance offsets
src_offset += actual_leds * channels_per_led;
dst_offset += leds_per_output * channels_per_led; // Full slot including padding
}
// ✅ Rest of function unchanged - transpose already works with padded buffer
create_transposed_led_output_optimized(
parallel_buffer_remapped,
parallel_buffer_repacked,
leds_per_output,
channels_per_led,
outputs
);
// ... existing transmission code ...
}Modified D_ParallelLEDDriver.h call sitevoid loop() override {
#ifdef ESP32_LEDSDRIVER
DriverNode::loop();
#ifndef CONFIG_IDF_TARGET_ESP32P4
if (ledsDriver.total_leds > 0) ledsDriver.showPixels(WAIT);
#else
uint8_t nrOfPins = min(layerP.nrOfLedPins, layerP.nrOfAssignedPins);
// Calculate max LEDs per pin
uint16_t maxLedsPerPin = 0;
for (uint16_t ledsPerPin : layerP.ledsPerPin) {
if (ledsPerPin > maxLedsPerPin) maxLedsPerPin = ledsPerPin;
}
// Call show_parlio with actual LED counts - padding handled internally
show_parlio(
pins,
layerP.lights.header.numChannels / layerP.lights.header.channelsPerLight, // actual total LEDs
layerP.lights.channelsD, // actual data, no padding needed
layerP.lights.header.channelsPerLight == 4,
nrOfPins,
maxLedsPerPin, // max LEDs per pin
layerP.ledsPerPin, // ✅ NEW: pass actual LED counts array
layerP.lights.header.offsetRed,
layerP.lights.header.offsetGreen,
layerP.lights.header.offsetBlue
);
#endif
#else
// ... existing code ...
#endif
}Update parlio.h signatureextern "C" void show_parlio(
const unsigned char *pins,
unsigned int length,
const unsigned char *buffer_in,
bool has_white,
unsigned int outputs,
unsigned int leds_per_output,
const uint16_t *actual_leds_per_pin, // ✅ NEW parameter
unsigned int offsetRed,
unsigned int offsetGreen,
unsigned int offsetBlue
);Benefits✅ No intermediate buffer allocation 🧠 Learnings used |
|
@coderabbitai, regarding padding parallel_buffer_remapped... This is still creating more memory. Go deeper , into create_transposed_led_output_optimized Go to the level where transpose is taking place into dma regions and pad there. And confirm this is a better solution (or not) |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 30606 @ewowi, you're absolutely right! Padding at the transpose level is better because it avoids creating any intermediate buffers. The Why This Is Superior✅ No extra memory allocation - uses existing DMA buffers ImplementationModified transpose_32_slicesinline void transpose_32_slices(
uint32_t (&transposed_slices)[32],
const uint8_t* input_buffer,
const uint32_t* cumulative_offsets, // ✅ NEW: pre-computed offsets per pin
const uint16_t* actual_leds_per_pin, // ✅ NEW: actual LED counts per pin
const uint32_t pixel_in_pin,
const uint32_t input_component,
const uint32_t pixels_per_pin, // Now: maxLedsPerPin
const uint32_t num_active_pins,
const uint32_t COMPONENTS_PER_PIXEL,
const uint32_t* waveform_cache,
const uint8_t* brightness_cache
) {
memset(transposed_slices, 0, sizeof(uint32_t) * 32);
for (uint32_t pin = 0; pin < num_active_pins; ++pin) {
// ✅ Skip this pin if pixel_in_pin exceeds its actual LED count (padding)
if (pixel_in_pin >= actual_leds_per_pin[pin]) {
// Pin bit stays 0 (already cleared by memset) - outputs black
continue;
}
// ✅ Use cumulative offset for this pin + pixel offset
const uint32_t component_idx = cumulative_offsets[pin] +
(pixel_in_pin * COMPONENTS_PER_PIXEL) +
input_component;
const uint8_t data_byte = brightness_cache[input_buffer[component_idx]];
const uint32_t waveform = waveform_cache[data_byte];
const uint32_t pin_bit = (1 << pin);
// ... existing bit extraction code unchanged ...
uint8_t b;
b = waveform & 0xFF;
if ((b >> 7) & 1) transposed_slices[0] |= pin_bit;
if ((b >> 6) & 1) transposed_slices[1] |= pin_bit;
// ... rest of bit extraction ...
}
}Modified create_transposed_led_output_optimizedvoid create_transposed_led_output_optimized(
const uint8_t* input_buffer,
uint16_t* output_buffer,
const uint32_t pixels_per_pin, // Now: maxLedsPerPin
const uint16_t* actual_leds_per_pin, // ✅ NEW: actual counts per pin
const uint32_t num_active_pins,
const bool is_rgbw,
const uint8_t offsetR,
const uint8_t offsetG,
const uint8_t offsetB
) {
// ... existing cache initialization ...
const uint32_t COMPONENTS_PER_PIXEL = is_rgbw ? 4 : 3;
// ✅ Pre-compute cumulative offsets for each pin in input_buffer
uint32_t cumulative_offsets[SOC_PARLIO_TX_UNIT_MAX_DATA_WIDTH];
cumulative_offsets[0] = 0;
for (uint32_t pin = 1; pin < num_active_pins; ++pin) {
cumulative_offsets[pin] = cumulative_offsets[pin-1] +
(actual_leds_per_pin[pin-1] * COMPONENTS_PER_PIXEL);
}
// ... existing output buffer setup ...
// --- Main Processing Loop ---
for (uint32_t pixel_in_pin = 0; pixel_in_pin < pixels_per_pin; ++pixel_in_pin) {
for (uint32_t component_in_pixel = 0; component_in_pixel < COMPONENTS_PER_PIXEL; ++component_in_pixel) {
const uint32_t input_component = component_map[component_in_pixel];
uint32_t transposed_slices[32];
// ... brightness_cache selection ...
// ✅ Pass new parameters to handle variable LEDs per pin
LedMatrixDetail::transpose_32_slices(
transposed_slices,
input_buffer,
cumulative_offsets, // ✅ NEW
actual_leds_per_pin, // ✅ NEW
pixel_in_pin,
input_component,
pixels_per_pin,
num_active_pins,
COMPONENTS_PER_PIXEL,
waveform_cache,
brightness_cache
);
// ... existing output packing unchanged ...
}
}
}Update parlio.h signatureextern "C" void create_transposed_led_output_optimized(
const uint8_t* input_buffer,
uint16_t* output_buffer,
const uint32_t pixels_per_pin,
const uint16_t* actual_leds_per_pin, // ✅ NEW
const uint32_t num_active_pins,
const bool is_rgbw,
const uint8_t offsetR,
const uint8_t offsetG,
const uint8_t offsetB
);Update show_parlio callcreate_transposed_led_output_optimized(
parallel_buffer_remapped,
parallel_buffer_repacked,
leds_per_output, // maxLedsPerPin
actual_leds_per_pin, // ✅ NEW: pass through from caller
outputs,
isRGBW,
offSetR,
offsetG,
offsetB
);D_ParallelLEDDriver.h stays simpleshow_parlio(
pins,
layerP.lights.header.numChannels / layerP.lights.header.channelsPerLight,
layerP.lights.channelsD, // Compact input - no padding needed
layerP.lights.header.channelsPerLight == 4,
nrOfPins,
maxLedsPerPin,
layerP.ledsPerPin, // Pass actual counts
// ... offsets ...
);Result✅ Zero extra memory - reuses existing DMA buffers This is indeed the optimal solution! 🧠 Learnings used |
backend ======= - Parallel LED Driver: call show_parlio with ledsPerPin array (instead of maxLedsPerPin), replace isRGB with channelsPerLight and add offsetW parlio (P4): - add max_leds_per_output and first_index_per_output - transpose_32_slices: add pixels_per_pin array (instead of max), new pixel_idx, component_idx and data_byte - create_transposed_led_output_optimized: pixels_per_pin array (instead of max), COMPONENTS_PER_PIXEL instead of is_rgbw, add offsetW, pixel_in_pin loop until max_leds_per_output - show_parlio: remove length check (all lengths are okay now), init max_leds_per_output and first_index_per_output, use components (channelsPerLight)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (4)
src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
55-56: Good documentation of the bounds-checking contract.This comment correctly documents that effect developers bear responsibility for keeping coordinates within
layer->sizebounds—consistent with the intentional omission of bounds checks in the unmappedsetLightpath for performance. Based on learnings, this is an explicit design decision for driving 100K+ LEDs.Minor style nit: consider
// NOTE: effects must ensure pos is within layer->size bounds (setRGB omits bounds checks for performance)for consistency with typical comment conventions.src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h (1)
61-65: Orphaned comment block.Lines 62-63 contain a comment "get max leds per pin" but no corresponding code follows it. The max calculation is actually performed inside
show_parlio(in parlio.cpp). Consider removing this misleading comment or clarifying that the max is computed internally byshow_parlio.Suggested fix
uint8_t nrOfPins = min(layerP.nrOfLedPins, layerP.nrOfAssignedPins); // LUTs are accessed directly within show_parlio via extern ledsDriver - - // get max leds per pin // No brightness parameter needed show_parlio(pins, layerP.lights.header.nrOfLights, layerP.lights.channelsD, layerP.lights.header.channelsPerLight, nrOfPins, layerP.ledsPerPin, layerP.lights.header.offsetRed, layerP.lights.header.offsetGreen, layerP.lights.header.offsetBlue, layerP.lights.header.offsetWhite);src/MoonLight/Nodes/Drivers/parlio.h (1)
17-18: Minor naming inconsistency:offSetRvsoffsetG,offsetB,offsetW.The first offset parameter uses
offSetR(capital S) while the others use lowercase (offsetG,offsetB,offsetW). Consider standardizing tooffsetRfor consistency.Suggested fix
-uint8_t show_parlio(uint8_t* parallelPins, uint32_t length, uint8_t* buffer_in, uint8_t components, uint8_t outputs, uint16_t* leds_per_output, uint8_t offSetR, uint8_t offsetG, uint8_t offsetB, uint8_t offsetW); +uint8_t show_parlio(uint8_t* parallelPins, uint32_t length, uint8_t* buffer_in, uint8_t components, uint8_t outputs, uint16_t* leds_per_output, uint8_t offsetR, uint8_t offsetG, uint8_t offsetB, uint8_t offsetW);src/MoonLight/Nodes/Drivers/parlio.cpp (1)
25-26: Global state introduces potential thread-safety concerns.The globals
max_leds_per_outputandfirst_index_per_outputare modified inshow_parlioand read intranspose_32_slices/create_transposed_led_output_optimized. While this is likely safe given the driver runs on a dedicated core (APP_CPU), document this assumption or consider passing these as parameters to avoid hidden coupling.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.hsrc/MoonLight/Nodes/Drivers/parlio.cppsrc/MoonLight/Nodes/Drivers/parlio.hsrc/MoonLight/Nodes/Effects/E__Sandbox.h
🧰 Additional context used
🧠 Learnings (10)
📓 Common learnings
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 84
File: src/MoonLight/Modules/ModuleLightsControl.h:367-379
Timestamp: 2025-12-28T10:28:24.157Z
Learning: In MoonLight PR #84: wrapping effectTask's layerP.loop() with monitorMutex while reading channelsE for monitor emission is thread-safe and prevents partial frames, but introduces serialization between effects rendering and monitor emission. Reading from channelsD (driver/front buffer) instead—with monitorMutex held only during the brief pointer swap—is superior because it maintains full parallelism (effects ║ monitor ║ driver), shows actual LED output, and minimizes mutex hold time.
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-06T13:45:03.067Z
Learning: For MoonLight on ESP32 at extreme loads (e.g., 49K channels @ 100 fps): Task watchdog can fire when AppDrivers/AppEffects process many nodes without yielding. Even with vTaskDelay(1) at loop end, the node iteration for-loops (PhysicalLayer::loopDrivers line 108-111, PhysicalLayer::loop line 73-75) can take >5 seconds without yielding, starving IDLE task (priority 0). Solution: add taskYIELD() or vTaskDelay(1) inside the node/layer iteration loops to give IDLE task chances to run and reset watchdog. Lowering task priority won't help since IDLE is already at 0 (lowest).
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-25T13:34:40.097Z
Learning: For the MoonLight LED system: Effects task can run on Core 0 (PRO_CPU) since it only performs computation (calculating colors, writing to channels array) without timing-critical hardware interaction. WiFi preemption is acceptable for pure computation tasks. Only the driver task (using DMA, I2S, LCD, PARLIO) requires Core 1 (APP_CPU) for uninterrupted timing-critical hardware operations.
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-12T17:15:32.074Z
Learning: In MoonLight's Monitor.ts (interface/src/routes/moonbase/monitor/monitor.ts), the camera distance calculation uses a padding multiplier in the formula: `cameraDistance = effectiveSize / (2 * Math.tan(fov / 2)) * padding`. A padding value of 5 (not 1.5) is needed because the effectiveSize calculation underestimates the actual visible model extent after scaling by [normalizedWidth, normalizedHeight, 1]. Larger padding values move the camera farther away, making the object smaller with more margin. Using 5 is safe and prevents clipping for all LED matrix aspect ratios.
📚 Learning: 2026-01-06T15:24:29.770Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.
Applied to files:
src/MoonLight/Nodes/Effects/E__Sandbox.hsrc/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.hsrc/MoonLight/Nodes/Drivers/parlio.cpp
📚 Learning: 2026-01-06T15:24:17.118Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:164-171
Timestamp: 2026-01-06T15:24:17.118Z
Learning: In VirtualLayer::setLight and VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking in the unmapped path (when indexV >= mappingTableSize or oneToOneMapping is true) is intentionally disabled for performance when driving very large LED counts (100K+). Effect developers are responsible for ensuring their code stays within bounds. This is an explicit design decision prioritizing performance over defensive checks in the hot path.
Applied to files:
src/MoonLight/Nodes/Effects/E__Sandbox.hsrc/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h
📚 Learning: 2025-12-18T15:15:57.828Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-18T15:15:57.828Z
Learning: In DripEffect (E_WLED.h), dropsSize must be set to layer->size.y (not layer->size.x) in onSizeChanged(), because the drops array is allocated as [layer->size.y][maxNumDrops]. Using layer->size.x causes out-of-bounds access when iterating over y-coordinates.
Applied to files:
src/MoonLight/Nodes/Effects/E__Sandbox.hsrc/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h
📚 Learning: 2025-12-28T10:28:24.157Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 84
File: src/MoonLight/Modules/ModuleLightsControl.h:367-379
Timestamp: 2025-12-28T10:28:24.157Z
Learning: In MoonLight PR #84: wrapping effectTask's layerP.loop() with monitorMutex while reading channelsE for monitor emission is thread-safe and prevents partial frames, but introduces serialization between effects rendering and monitor emission. Reading from channelsD (driver/front buffer) instead—with monitorMutex held only during the brief pointer swap—is superior because it maintains full parallelism (effects ║ monitor ║ driver), shows actual LED output, and minimizes mutex hold time.
Applied to files:
src/MoonLight/Nodes/Effects/E__Sandbox.hsrc/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/src/**/*.{h,cpp} : Modify C++ backend code in the `src/` and `lib/` directories for ESP32 development
Applied to files:
src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h
📚 Learning: 2026-01-06T13:45:03.067Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-06T13:45:03.067Z
Learning: For MoonLight on ESP32 at extreme loads (e.g., 49K channels @ 100 fps): Task watchdog can fire when AppDrivers/AppEffects process many nodes without yielding. Even with vTaskDelay(1) at loop end, the node iteration for-loops (PhysicalLayer::loopDrivers line 108-111, PhysicalLayer::loop line 73-75) can take >5 seconds without yielding, starving IDLE task (priority 0). Solution: add taskYIELD() or vTaskDelay(1) inside the node/layer iteration loops to give IDLE task chances to run and reset watchdog. Lowering task priority won't help since IDLE is already at 0 (lowest).
Applied to files:
src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h
📚 Learning: 2025-12-07T14:16:03.228Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-07T14:16:03.228Z
Learning: VirtualLayer mappingTableSize is uint16_t with a maximum of 65535 LEDs. If a user defines a space where size.x * size.y * size.z exceeds 65535, the code accepts the overflow and performs mapping partially. This is an explicit design decision balancing performance and memory constraints.
Applied to files:
src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.hsrc/MoonLight/Nodes/Drivers/parlio.cpp
📚 Learning: 2025-12-25T13:34:40.097Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-25T13:34:40.097Z
Learning: For the MoonLight LED system: Effects task can run on Core 0 (PRO_CPU) since it only performs computation (calculating colors, writing to channels array) without timing-critical hardware interaction. WiFi preemption is acceptable for pure computation tasks. Only the driver task (using DMA, I2S, LCD, PARLIO) requires Core 1 (APP_CPU) for uninterrupted timing-critical hardware operations.
Applied to files:
src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h
📚 Learning: 2026-01-12T17:15:32.074Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-12T17:15:32.074Z
Learning: In MoonLight's Monitor.ts (interface/src/routes/moonbase/monitor/monitor.ts), the camera distance calculation uses a padding multiplier in the formula: `cameraDistance = effectiveSize / (2 * Math.tan(fov / 2)) * padding`. A padding value of 5 (not 1.5) is needed because the effectiveSize calculation underestimates the actual visible model extent after scaling by [normalizedWidth, normalizedHeight, 1]. Larger padding values move the camera farther away, making the object smaller with more margin. Using 5 is safe and prevents clipping for all LED matrix aspect ratios.
Applied to files:
src/MoonLight/Nodes/Drivers/parlio.cpp
🧬 Code graph analysis (2)
src/MoonLight/Nodes/Drivers/parlio.h (1)
src/MoonLight/Nodes/Drivers/parlio.cpp (1)
show_parlio(273-273)
src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h (1)
src/MoonLight/Nodes/Drivers/parlio.cpp (1)
show_parlio(273-273)
🪛 Cppcheck (2.19.0)
src/MoonLight/Nodes/Drivers/parlio.cpp
[error] 414-414: Division by zero.
(zerodiv)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (4)
src/MoonLight/Nodes/Drivers/parlio.cpp (4)
33-39: LGTM - Elegant padding approach.The conditional at line 39 implementing padding by returning 0 when
pixel_in_pin >= pixels_per_pin[pin]is a memory-efficient solution that avoids extra buffer allocations. This directly writes padded zeros into the DMA buffer during transpose.
155-156: LGTM - Clean API expansion for variable components.The updated signature properly exposes
COMPONENTS_PER_PIXELandoffsetWparameters, enabling proper handling of both RGB and RGBW LED strips with configurable color ordering.
379-406: LGTM - Flexible component-based pixel handling.The shift from boolean
isRGBWtocomponentscount properly generalizes the code for different LED strip types. The remapping and transposition calls correctly propagate the component count and offset parameters.
291-299: No array bounds issue.
first_index_per_output[16]is correctly sized. The outputs parameter is capped toSOC_PARLIO_TX_UNIT_MAX_DATA_WIDTHat line 289, which equals 16 on all PARLIO-supporting platforms (ESP32-P4, ESP32-C6). The loop at line 296 iterates up tooutputs(max 16), accessing indices [0, 15], which is safe for the 16-element array.Likely an incorrect or invalid review comment.
Summary by CodeRabbit
New Features
Bug Fixes
Style
✏️ Tip: You can customize this high-level summary in your review settings.