You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
HUB75: Some 2D effects missing pixel trails in v16.0 (Black Hole, Lissajous, Spaceships) #5664
In WLED 16.0.0, the "Black hole" effect does not produce the same long pixels trails as in previous releases (trails used to be up to ~1/3 turn long)
Changing the "fade rate" slide has almost zero effect on the trail length (never longer than 2-3 pixels).
The "Blur" checkbox does affect brightness, but also does not help to get longer pixels trails.
Tested with a 64x32 HUB75 matrix (esp32-S3), gamma = 2.2, no white balance correction; target FPS = unlimited.
Edit: this could be another case of brightness-gamma adjustments, similar to
@coderabbitai please check if the Segment::blur() and Segment::fade_out() / Segment::fadeToBlackBy() are still producing the same results, considering the different gamma / rendering architectures of 16.0 and 0.15.x.
16.0: effects operate in "non-gamma" space, the pixels pipeline applies scale(gamma32(pixelColor), brightness) when passing pixels to the driver level, after all effects have calculated their pixels. output drivers do not perform brightness scaling or gamma adjustments.
0.15.0 effects operate in "gamma-corrected" space, some effects have "hand-tuned" parameters for blur() and fade_out() to achieve pixels trails. Pixels from effects are passed on to the driver level; the driver scales brightness but does not re-apply gamma.
@DedeHai i think we've discussed the topic already, just wanted to check if the rabbit comes up with new aspects or consequences that we did not look into yet.
#!/bin/bash# Find functions in FX_fcn.cpp
rg -n "void Segment::blur\|void Segment::fade_out\|void Segment::fadeToBlackBy" wled00/FX_fcn.cpp
# Also check the header
rg -n "blur\|fade_out\|fadeToBlackBy" wled00/FX.h | head -30
Length of output: 469
🏁 Script executed:
#!/bin/bash# Get full blur implementation
sed -n '1082,1140p' wled00/FX_fcn.cpp
echo"---"# Find fade_out and fadeToBlackBy
rg -n "fade_out\|fadeToBlackBy" wled00/FX_fcn.cpp | head -30
#!/bin/bashecho"=== fade_out, fadeToBlackBy, fadeToSecondaryBy implementations ==="
rg -n "fade_out\|fadeToBlackBy\|fadeToSecondaryBy" wled00/FX_fcn.cpp
echo""echo"=== setPixelColor vs setPixelColorRaw ==="
rg -n "setPixelColorRaw\|void Segment::setPixelColor" wled00/FX_fcn.cpp | head -20
echo""echo"=== fast_color_scale / n() in colors.h ==="
sed -n '1,50p' wled00/colors.h
echo""echo"=== Output pipeline around line 1735-1760 ==="
sed -n '1720,1770p' wled00/FX_fcn.cpp
Length of output: 5771
🏁 Script executed:
#!/bin/bashecho"=== Line context around fade functions in FX_fcn.cpp ==="
grep -n "fade_out\|fadeToBlack\|fadeToSecondary" wled00/FX_fcn.cpp
echo""echo"=== getPixelColorRaw returns n(i) - let's see setPixelColorRaw ==="
rg -n "setPixelColorRaw" wled00/FX.h | head -10
echo""echo"=== Segment::setPixelColor in FX_fcn.cpp ==="
rg -n "Segment::setPixelColor" wled00/FX_fcn.cpp | head -10
Length of output: 1269
🏁 Script executed:
#!/bin/bashecho"=== fade_out (line 1045), fadeToSecondaryBy (line 1069), fadeToBlackBy (line 1076) ==="
sed -n '1045,1085p' wled00/FX_fcn.cpp
echo""echo"=== setPixelColor (line 727) - first ~40 lines ==="
sed -n '727,780p' wled00/FX_fcn.cpp
echo""echo"=== color_fade definition in colors.h ==="
rg -n "color_fade\|color_add\|color_blend" wled00/colors.h | head -20
Length of output: 4714
@softhack007, great question. After reviewing all three functions and the output pipeline, here is the analysis:
Analysis: blur(), fade_out(), fadeToBlackBy() in v0.16.0 vs v0.15.x
All three operate exclusively on the raw pixel buffer (getPixelColorRaw / setPixelColorRaw → direct pixels[i] access, no gamma). The critical difference comes from what those pixel values represent and what happens downstream.
Pipeline difference recap
v0.15.x
v0.16.0
Pixel buffer content
Gamma-encoded (~sRGB) values
Linear light values
Driver-level processing
Scales brightness (linear multiply)
gamma32(c) applied, then passes to bus
LED output for stored value v
v
gamma32(v) ≈ v^2.8 / 255^1.8
1. fadeToBlackBy(fadeBy) — Trails fade faster in v0.16.0
v0.15.x: pixel values are gamma-encoded. The raw linear multiply produces a fade that tracks approximately with perceived brightness steps because the stored values already encode the gamma curve.
v0.16.0: pixel values are linear. The same linear multiply is correct physics, but the output gamma32() compresses lower values more aggressively. A pixel at mid-value fades quickly in absolute terms, but gamma32 makes the low end of the curve map near-zero faster — trails appear to extinguish sooner for the same fadeBy value.
Net effect: for the same fadeBy parameter, fade trails are visually shorter/dimmer in v0.16.0. Effects with hand-tuned fadeBy values (e.g. comet trails) will look noticeably different.
v0.15.x: blur is performed on gamma-encoded values → gamma-space averaging → result slightly darker than linear average (nonlinear mixing).
v0.16.0: blur is performed on linear values → linear averaging is physically correct, but the linear average is then gamma-compressed at output. Since gamma compresses higher values more than proportionally, bright blurred halos will appear subtly broader/brighter vs v0.15.x for the same blur_amount.
Net effect: blur looks more physically correct in v0.16.0 but differs from v0.15.x tuning. Effects that relied on a specific blur_amount for their "glow radius" will see it look slightly brighter.
3. fade_out(rate) and fadeToSecondaryBy(fadeBy) — Domain mismatch with colors[1]
This is the most problematic case. colors[1] is SEGCOLOR(1) — a raw sRGB value from the UI color picker, stored without any gamma transformation. In v0.16.0 the pixel buffer holds linear values, so:
v0.15.x: both pixel buffer values and colors[1] were approximately gamma-encoded → interpolation was in a consistent domain.
v0.16.0: pixel buffer is linear, colors[1] is sRGB → mixing linear and sRGB values in the same interpolation. The interpolated result will be in neither space correctly. Once output gamma32() is applied, the secondary color endpoint will be doubly gamma-corrected (once implicitly via sRGB encoding, once at output), making it appear dimmer than intended.
Summary table
Function
Behavior change in v0.16.0
Severity
fadeToBlackBy()
Trails die faster; same fadeBy produces shorter trails
Medium — hand-tuned effects differ
fadeToSecondaryBy()
Secondary color endpoint is effectively double-gamma'd
Medium
fade_out()
Same domain mismatch; secondary color too dark
Medium
blur()
Slightly brighter/broader halos for same blur_amount
Low–Medium
Potential fixes
fadeToBlackBy()/blur(): These are operating correctly in linear space for v0.16.0. The visual mismatch vs v0.15.x is inherent to the architecture change. Effects relying on specific trail lengths may need their fade/blur parameters retuned.
fade_out() / fadeToSecondaryBy(): The secondary color domain mismatch could be corrected by pre-converting colors[1] with gamma32inv() before the interpolation, so both endpoints are in linear space before setPixelColorRaw:
uint32_t target = NeoGammaWLEDMethod::inverseGamma32(colors[1]);
// then use target instead of colors[1] in the blend
This would make the fade-to-secondary behave consistently with the rest of the linear pipeline.
Note: This analysis uses the long-term memory context about WLED v0.16.0's gamma pipeline (gamma32 applied at output, SEGCOLOR values are raw sRGB from UI picker), which was established in prior sessions.
🧠 Learnings used
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-11T21:39:46.794Z
Learning: In WLED v0.16.0 FX.cpp, the following effects directly compute RGB colors without palettes and need gamma8inv() to restore v0.15.0 visual behavior due to the new gamma pipeline: (1) mode_DJLight - CRGB(fftResult/2); (2) mode_2Dmatrix - hardcoded RGBW32(175,255,175,0)/RGBW32(27,130,39,0) default Matrix green; (3) mode_traffic_light - hardcoded 0x00FF0000/0x00EECC00/0x0000FF00; (4) mode_2DFunkyPlank - CHSV(fftResult,255,map(fftResult,0,255,10,255)) where the v channel needs gamma8inv(); (5) mode_waterfall peak indicator CRGB(CHSV(92,92,92)) where v=92 needs gamma8inv(). Separate bug: mode_tv_simulator applies gamma8() manually in both v0.15.0 AND current, causing double gamma in v0.16.0 - fix is to REMOVE the 3 explicit gamma8() calls at the nr/ng/nb assignments, not to add gamma8inv(). Already correctly handled: mode_pride_2015 (has gamma32inv()) and mode_2DDistortionWaves (manual gamma8 removed, pipeline handles it).
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-17T16:30:07.598Z
Learning: In WLED 16.0.0 twinklefox_base() in wled00/FX.cpp, bg = gamma32inv(bg) applied to SEGCOLOR(1) is incorrect — it over-brightens the background because SEGCOLOR values are sRGB-encoded and the output pipeline already applies gamma32() to compensate. The correct approach is to leave bg unmodified for setPixelColor() output. For the deltabright brightness comparison, since c from gamma-cleaned palettes is in linear space while bg (raw sRGB) is not, the comparison can be made consistent by converting c to sRGB-like space: CRGBW cc2 = gamma32inv(c); cbright = cc2.getAverageLight().
Learnt from: DedeHai
Repo: wled/WLED PR: 4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR `#4798`, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-11T21:43:36.003Z
Learning: In WLED's wled00/FX.cpp, the following effects build colors directly from computed/hardcoded values without using palettes/rainbow/user-defined colors, and have identical color logic in v0.15.0 without gamma8inv. In v0.16.0, these effects need gamma8inv() wrapping to restore previous appearance due to the new render-path gamma correction:
1. mode_traffic_light - hardcoded 0xFF0000, 0xEECC00, 0x00FF00
2. mode_pacifica - hardcoded floor CRGB(2,6,10) and CRGB(2,5,7); overage highlights
3. mode_2Dmatrix - hardcoded default RGBW32(175,255,175,0) and RGBW32(27,130,39,0)
4. mode_2DSunradiation - HeatColor() output goes directly to setPixelColorXY
5. mode_2DFunkyPlank - CHSV(fftResult[band], 255, v) audio-reactive colors
6. mode_waterfall - CHSV(92,92,92) peak indicator (minor)
NOTE: mode_2Ddistortionwaves is NOT a candidate - v0.15.0 applied explicit gamma8() in the effect itself (lines 7581-7583), which was correctly removed in v0.16.0 to rely on render-path gamma instead. Pride2015/ColorWaves already applies gamma32inv() explicitly.
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-17T16:30:07.598Z
Learning: In WLED 16.0.0 (wled00/FX_fcn.cpp), gamma32() is applied automatically in the output pipeline (Line 1741-1743) to all values stored via setPixelColor(). Therefore, user-picked colors from SEGCOLOR/SEGCOLOR(x) are already sRGB-encoded (gamma ~2.2), and the output gamma32() approximately undoes that encoding, resulting in correct LED luminance. Effects should NOT apply gamma32inv() to SEGCOLOR values before passing to setPixelColor() — doing so would over-brighten them. Only effect-generated linear/perceptual values benefit from gamma32inv() pre-processing (e.g. palette colors, computed brightness values).
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-17T16:25:46.961Z
Learning: In WLED (wled00/), user-defined segment colors (`SEGCOLOR(x)`, stored in `Segment::colors[]`) are raw sRGB values from the UI color picker — no gamma transformation is applied anywhere in the ingestion path (confirmed in json.cpp and FX_fcn.cpp beginDraw()). However, treating them as "gamma-encoded" requiring `gamma32inv()` before `setPixelColor()` is imprecise: sRGB gamma (~2.2) and WLED's LED gamma (~2.2–2.8) are different curves. From the LED pipeline's perspective, sRGB values are close to linear. The safety of applying `gamma32inv()` to SEGCOLOR values depends on whether WLED's configured LED gamma matches sRGB's gamma exactly. softhack007 confirmed this.
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-11T20:53:20.707Z
Learning: In WLED, `gamma8inv()` (macro for `NeoGammaWLEDMethod::rawInverseGamma8()`) is an established pattern for pre-compensating values that will later pass through the gamma correction pipeline in `show()`. It is already used in FX_fcn.cpp (Line 1419 for opacity) and FXparticleSystem.cpp (Lines 681, 805, 1534-1535 for pixel brightness). The function is a no-op when `gammaCorrectCol` is false, making it self-consistent. When effects in `wled00/FX.cpp` build colors directly from raw data (e.g., FFT values) and those colors appear too dim in v0.16.0 vs v0.15.0, the fix is to wrap each component in `gamma8inv()` to compensate for the new gamma correction step added in the render path between strip and bus level in v0.16.0.
Learnt from: DedeHai
Repo: wled/WLED PR: 4615
File: wled00/FXparticleSystem.cpp:678-680
Timestamp: 2026-03-29T06:10:40.732Z
Learning: In WLED's FXparticleSystem.cpp, `gamma8inv()` is a lookup-table call that applies the inverse of the gamma curve — it is NOT a no-op (identity function) when `gammaCorrectCol` is false. All calls to `gamma8inv()` on particle brightness values (pxlbrightness[] in renderParticle 2D standard render, the large-particle ellipse path in renderLargeParticle 2D, and pxlbrightness[] in renderParticle 1D) must be guarded by `if (gammaCorrectCol)` to avoid pre-distorting brightness when gamma correction is disabled. Without the guard, particles render noticeably darker/nonlinear in gamma-disabled configurations because the inverse correction is applied but no forward gamma correction follows in show().
Learnt from: softhack007
Repo: wled/WLED PR: 5635
File: wled00/FX.cpp:2680-2683
Timestamp: 2026-05-20T11:34:59.048Z
Learning: In WLED v16.0.0, colors returned from ColorFromPalette/SEGMENT.color_from_palette are already in linear (gamma-free) space. Do not apply gamma32inv() to palette-derived colors (e.g., Twinklefox twinkle pixels) before brightness calculations; doing so will artificially brighten and can alter hue. Background handling may need gamma8/32 inversions as applicable, but palette colors should be left as-is for linear comparisons.
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-17T16:32:53.376Z
Learning: In WLED (wled00/FX_fcn.cpp), `setPixelColor()` applies `gamma32()` forward gamma correction at output (line ~1742). User-picked segment colors (`SEGCOLOR(x)`) are stored as raw sRGB values with no gamma transformation in the ingestion path. Since sRGB gamma (~2.2) approximates WLED LED gamma (~2.2-2.8), user-picked colors should be passed to `setPixelColor()` WITHOUT applying `gamma32inv()` first. Applying `gamma32inv` to a user-picked SEGCOLOR before setPixelColor causes over-brightness because it decodes the sRGB encoding and then the output pipeline re-applies gamma, resulting in a net perceptual boost beyond the user's intention.
Learnt from: DedeHai
Repo: wled/WLED PR: 5601
File: wled00/FX_fcn.cpp:301-323
Timestamp: 2026-05-14T17:13:17.872Z
Learning: In `wled00/FX_fcn.cpp`, inside `Segment::startTransition()`, the `segmentCopy && !_t->_oldSegment` branch intentionally does NOT refresh `_t->_colors`, `_t->_bri`, or `_t->_cct` to the current blended values before restarting the transition timer and creating `_t->_oldSegment`. The `_oldSegment`-based path handles visual continuity differently from the pure FADE path, and this asymmetry is deliberate. Do not flag this as a bug or suggest syncing those fields in that branch.
Update: the "Lissajous" effect has similar problems in v16.0: the "Fade Rate" slider has almost zero effect (in 0.15.0 you could smear the curve), and the "Blur" slider just dims the effect down, without much visible blur.
changed the title [-]Black hole effect: missing pixel trails in v16.0[/-][+]Black Hole and Lissajous effects: missing pixel trails in v16.0[/+]on Jun 3, 2026
@coderabbitai please compare the Segment::blur() implementation between main and 0_15_x branches. Are there any code changes in the blur() function that could explain the different behaviour?
changed the title [-]Black Hole and Lissajous effects: missing pixel trails in v16.0[/-][+]Some 2D effects missing pixel trails in v16.0 (Black Hole, Lissajous, Spaceships) [/+]on Jun 3, 2026
#!/bin/bashecho"=== blur() on current (main) branch ==="
grep -n "void Segment::blur" wled00/FX_fcn.cpp
# get line number and show the function
LINE=$(grep -n "void Segment::blur" wled00/FX_fcn.cpp | head -1 | cut -d: -f1)echo"blur starts at line $LINE"
sed -n "${LINE},$((LINE+80))p" wled00/FX_fcn.cpp
echo""echo"=== blur() on 0_15_x branch ==="
git show origin/0_15_x:wled00/FX_fcn.cpp | grep -n "void Segment::blur"| head -5
LINE15=$(git show origin/0_15_x:wled00/FX_fcn.cpp | grep -n "void Segment::blur"| head -1 | cut -d: -f1)echo"blur starts at line $LINE15 on 0_15_x"
git show origin/0_15_x:wled00/FX_fcn.cpp | sed -n "${LINE15},$((LINE15+80))p"echo""echo"=== git diff blur() between branches ==="
git diff origin/0_15_x..HEAD -- wled00/FX_fcn.cpp | grep -A 5 -B 5 "blur"
Length of output: 5084
🏁 Script executed:
#!/bin/bashecho"=== Available remote branches ==="
git branch -r | head -30
echo""echo"=== Fetch 0_15_x from GitHub directly ==="
gh api repos/wled/WLED/contents/wled00/FX_fcn.cpp?ref=0_15_x --jq '.download_url'| xargs curl -s > /tmp/FX_fcn_0_15_x.cpp 2>&1echo"File size: $(wc -l < /tmp/FX_fcn_0_15_x.cpp)"echo""echo"=== blur() on 0_15_x ==="
grep -n "void Segment::blur" /tmp/FX_fcn_0_15_x.cpp
LINE15=$(grep -n "void Segment::blur" /tmp/FX_fcn_0_15_x.cpp | head -1 | cut -d: -f1)echo"blur starts at line $LINE15"
sed -n "${LINE15},$((LINE15+70))p" /tmp/FX_fcn_0_15_x.cpp
echo""echo"=== Also check blur2D on 0_15_x ==="
grep -n "void Segment::blur2D\|void.*blur2D" /tmp/FX_fcn_0_15_x.cpp
LINE2D=$(grep -n "void Segment::blur2D" /tmp/FX_fcn_0_15_x.cpp | head -1 | cut -d: -f1)if [ -n"$LINE2D" ];then
sed -n "${LINE2D},$((LINE2D+60))p" /tmp/FX_fcn_0_15_x.cpp
fiecho""echo"=== Also check blur2D on main ==="
grep -n "void Segment::blur2D\|void.*blur2D" wled00/FX_fcn.cpp
LINE2D_MAIN=$(grep -n "void Segment::blur2D" wled00/FX_fcn.cpp | head -1 | cut -d: -f1)if [ -n"$LINE2D_MAIN" ];then
sed -n "${LINE2D_MAIN},$((LINE2D_MAIN+60))p" wled00/FX_fcn.cpp
fiecho""echo"=== Check getPixelColorRaw / setPixelColorRaw on both ==="
grep -n "getPixelColorRaw\|setPixelColorRaw" /tmp/FX_fcn_0_15_x.cpp | head -10
echo"---main---"
grep -n "getPixelColorRaw\|setPixelColorRaw" wled00/FX_fcn.cpp | head -10
Length of output: 5407
🏁 Script executed:
#!/bin/bashecho"=== blur2D in main branch ==="
rg -rn "void.*blur2D\|Segment::blur2D" wled00/
echo""echo"=== blur2D implementation on main ==="
rg -rn "blur2D" wled00/FX_fcn.cpp | head -10
LINE_B2D=$(grep -n "void Segment::blur2D\|Segment::blur2D" wled00/FX_fcn.cpp | head -1 | cut -d: -f1)if [ -n"$LINE_B2D" ];then
sed -n "${LINE_B2D},$((LINE_B2D+80))p" wled00/FX_fcn.cpp
fi# Also check FX_2Dfunctions.cpp or similar
fd "FX_2D" wled00/ --exec grep -n "void.*blur2D" {}
echo""echo"=== blur2D in 0_15_x ==="
gh api "repos/wled/WLED/contents/wled00/FX_fcn.cpp?ref=0_15_x" --jq '.download_url'| xargs curl -s | grep -n "blur2D"| head -20
echo""echo"=== Check if blur2D is in FX.h header (both versions) ==="
grep -n "blur2D\|blur(" wled00/FX.h | head -20
echo"---0_15_x header---"
gh api "repos/wled/WLED/contents/wled00/FX.h?ref=0_15_x" --jq '.download_url'| xargs curl -s | grep -n "blur2D\|blur("| head -20
echo""echo"=== color_add definition on main (check for overflow/saturating parameter) ==="
rg -n "inline.*color_add\|uint32_t color_add\|color_add" wled00/colors.h | head -20
echo""echo"=== color_add in 0_15_x colors.h ==="
gh api "repos/wled/WLED/contents/wled00/colors.h?ref=0_15_x" --jq '.download_url'| xargs curl -s | grep -n "color_add"| head -20
Length of output: 2982
🏁 Script executed:
#!/bin/bashecho"=== blur2D implementation on main (line 246) ==="
sed -n '246,330p' wled00/FX_fcn.cpp
echo""echo"=== color_add definition on main ==="
rg -rn "inline.*color_add\|uint32_t color_add" wled00/ | head -10
rg -rn "color_add" wled00/colors.h | head -10
rg -rn "^.*color_add" wled00/FX.h | head -10
echo""echo"=== fast_color_scale definition ==="
rg -rn "fast_color_scale" wled00/ | head -10
echo""echo"=== color_add / fast_color_scale in wled00 headers ==="
rg -rn "color_add\|fast_color_scale" wled00/wled.h wled00/const.h 2>/dev/null | head -20
echo""echo"=== Search all cpp/h files for color_add definition ==="
rg -rn "^inline.*color_add\|^uint32_t color_add\|^static.*color_add" wled00/ | head -10
echo""echo"=== Get blur2D in 0_15_x ==="
gh api "repos/wled/WLED/contents/wled00/FX_fcn.cpp?ref=0_15_x" --jq '.download_url'| xargs curl -s > /tmp/FX_fcn_0_15_x.cpp
grep -n "void Segment::blur2D" /tmp/FX_fcn_0_15_x.cpp
LINE_2D=$(grep -n "void Segment::blur2D" /tmp/FX_fcn_0_15_x.cpp | head -1 | cut -d: -f1)if [ -n"$LINE_2D" ];then
sed -n "${LINE_2D},$((LINE_2D+80))p" /tmp/FX_fcn_0_15_x.cpp
fi
Length of output: 6623
🏁 Script executed:
#!/bin/bashecho"=== Find blur2D in main (correct line) ==="
grep -n "void Segment::blur2D" wled00/FX_fcn.cpp
LINE=$(grep -n "void Segment::blur2D" wled00/FX_fcn.cpp | head -1 | cut -d: -f1)echo"Line: $LINE"
sed -n "${LINE},$((LINE+70))p" wled00/FX_fcn.cpp
echo""echo"=== Find blur2D in all main branch files ==="
rg -rn "void Segment::blur2D" wled00/
echo""echo"=== colors.h - full content for color_add / fast_color_scale ==="
cat wled00/colors.h
echo""echo"=== blur2D in 0_15_x — check all files ==="forfin FX_fcn.cpp FX.cpp FX.h;doecho"--- $f ---"
gh api "repos/wled/WLED/contents/wled00/${f}?ref=0_15_x" --jq '.download_url'| xargs curl -s | grep -n "void Segment::blur2D\|void blur2D"| head -5
doneecho""echo"=== Get blur2D impl from 0_15_x FX_fcn.cpp ==="
gh api "repos/wled/WLED/contents/wled00/FX_fcn.cpp?ref=0_15_x" --jq '.download_url'| xargs curl -s > /tmp/fx15.cpp
LINE2D=$(grep -n "void Segment::blur2D" /tmp/fx15.cpp | head -1 | cut -d: -f1)echo"0_15_x blur2D at line: $LINE2D"
[ -n"$LINE2D" ] && sed -n "${LINE2D},$((LINE2D+70))p" /tmp/fx15.cpp
Length of output: 12680
🏁 Script executed:
#!/bin/bashecho"=== blur2D in main FX_2Dfcn.cpp ==="
grep -n "void Segment::blur2D\|void.*blur2D" wled00/FX_2Dfcn.cpp
LINE=$(grep -n "void Segment::blur2D" wled00/FX_2Dfcn.cpp | head -1 | cut -d: -f1)echo"Line: $LINE"
sed -n "${LINE},$((LINE+80))p" wled00/FX_2Dfcn.cpp
echo""echo"=== Find blur2D in 0_15_x — list all cpp files ==="
gh api "repos/wled/WLED/git/trees/0_15_x?recursive=1" --jq '.tree[] | select(.path | test("wled00/.*\\.cpp$")) | .path'2>/dev/null | head -30
echo""echo"=== Try FX_2Dfcn.cpp in 0_15_x ==="
gh api "repos/wled/WLED/contents/wled00/FX_2Dfcn.cpp?ref=0_15_x" --jq '.download_url'2>/dev/null | xargs curl -s 2>/dev/null | grep -n "void Segment::blur2D\|void.*blur2D"| head -5
LINE2D_15=$(gh api "repos/wled/WLED/contents/wled00/FX_2Dfcn.cpp?ref=0_15_x" --jq '.download_url'2>/dev/null | xargs curl -s 2>/dev/null | grep -n "void Segment::blur2D"| head -1 | cut -d: -f1)echo"Line in 0_15_x FX_2Dfcn.cpp: $LINE2D_15"if [ -n"$LINE2D_15" ];then
gh api "repos/wled/WLED/contents/wled00/FX_2Dfcn.cpp?ref=0_15_x" --jq '.download_url'| xargs curl -s > /tmp/fx2d_15.cpp
sed -n "${LINE2D_15},$((LINE2D_15+80))p" /tmp/fx2d_15.cpp
fi
Length of output: 8400
@softhack007, here is the full comparison. I found three concrete code-level regressions and one architectural change:
blur() and blur2D(): main vs 0_15_x
Bug 1 — seep formula in smear mode (regression in both blur() and blur2D())
Code
smear=false
smear=true
v0.15.x
seep = blur_amount >> (1 + smear)
blur_amount / 2
blur_amount / 4
v0.16.0
seep = blur_amount >> 1
blur_amount / 2
blur_amount / 2 ← doubled
When smear=true, v0.16.0 spreads twice as much color into each neighbor compared to v0.15.x. This is a direct regression — effects using blur(..., smear=true) (e.g. comet trails, Lissajous smear) will look significantly brighter/broader in v0.16.0. The fix:
// FX_fcn.cpp (blur) and FX_2Dfcn.cpp (blur2D) — restore original smear logic:uint8_t seep = blur_amount >> (1 + smear); // v0.15.x formula
Bug 2 — color_addpreserveCR flag removed
Code
v0.15.x
color_add(curnew, carryover, true) — preserveCR=true: on overflow, scales result back proportionally to preserve hue
v0.16.0
color_add(cur, carryover) — preserveCR=false: per-channel clamp, hue shifts toward white on overflow
In smear mode with bright pixels, v0.15.x preserved hue when neighbor contributions summed above 255; v0.16.0 clips per-channel and can produce white-shifted results.
Architectural change — linear vs gamma-encoded pixel domain
Pixel read/write
Domain
v0.15.x
getPixelColor() / setPixelColor()
Gamma-encoded (~sRGB) values in buffer; driver scales brightness
v0.16.0
getPixelColorRaw() / setPixelColorRaw()
Linear light values in buffer; gamma32() applied once at output
Blur arithmetic (keep * self + seep * neighbors) is physically correct in linear space (v0.16.0), but the gamma curve is very steep at low values. After blurring in linear space, gamma32() at output compresses low-value pixels much harder than v0.15.x did — fading trails extinguish faster for the same blur_amount. This explains the "dims the effect down" complaint for Lissajous. There is no simple per-call fix for this; effects need their blur_amount parameters retuned.
blur() calls blur2D(blur_amount, blur_amount, smear) symmetrically, so for effects that call blur() directly there is no net difference. The new signature allows asymmetric 2D blurs.
Linear-domain blur + gamma at output compresses low values more steeply
Hue shifts at high blur with bright pixels
preserveCR flag removed from color_add calls
The seep formula is an actionable bug fix. The other two are consequences of the deliberate architectural change to linear-space rendering.
✏️ Learnings added
Learnt from: softhack007
URL: https://github.com/wled/WLED/issues/5664
Timestamp: 2026-06-03T22:33:06.467Z
Learning: In WLED (wled00/FX_fcn.cpp and wled00/FX_2Dfcn.cpp), the v0.16.0 `blur()` and `blur2D()` functions have a regression vs v0.15.x in smear mode: v0.16.0 uses `seep = blur_amount >> 1` unconditionally, whereas v0.15.x used `seep = blur_amount >> (1 + smear)`. When smear=true, v0.16.0 spreads twice as much color into each neighbor pixel as v0.15.x did. The fix is to restore the `>> (1 + smear)` formula in both functions. Additionally, v0.15.x used `color_add(..., true)` (preserveCR=true, proportional scale-back on overflow) while v0.16.0 uses `color_add(...)` without it, causing possible hue shifts toward white at high blur values. These changes explain why smear-based comet/trail effects look different in v0.16.0.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-17T16:30:07.598Z
Learning: In WLED 16.0.0 twinklefox_base() in wled00/FX.cpp, bg = gamma32inv(bg) applied to SEGCOLOR(1) is incorrect — it over-brightens the background because SEGCOLOR values are sRGB-encoded and the output pipeline already applies gamma32() to compensate. The correct approach is to leave bg unmodified for setPixelColor() output. For the deltabright brightness comparison, since c from gamma-cleaned palettes is in linear space while bg (raw sRGB) is not, the comparison can be made consistent by converting c to sRGB-like space: CRGBW cc2 = gamma32inv(c); cbright = cc2.getAverageLight().
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-11T21:39:46.794Z
Learning: In WLED v0.16.0 FX.cpp, the following effects directly compute RGB colors without palettes and need gamma8inv() to restore v0.15.0 visual behavior due to the new gamma pipeline: (1) mode_DJLight - CRGB(fftResult/2); (2) mode_2Dmatrix - hardcoded RGBW32(175,255,175,0)/RGBW32(27,130,39,0) default Matrix green; (3) mode_traffic_light - hardcoded 0x00FF0000/0x00EECC00/0x0000FF00; (4) mode_2DFunkyPlank - CHSV(fftResult,255,map(fftResult,0,255,10,255)) where the v channel needs gamma8inv(); (5) mode_waterfall peak indicator CRGB(CHSV(92,92,92)) where v=92 needs gamma8inv(). Separate bug: mode_tv_simulator applies gamma8() manually in both v0.15.0 AND current, causing double gamma in v0.16.0 - fix is to REMOVE the 3 explicit gamma8() calls at the nr/ng/nb assignments, not to add gamma8inv(). Already correctly handled: mode_pride_2015 (has gamma32inv()) and mode_2DDistortionWaves (manual gamma8 removed, pipeline handles it).
Learnt from: DedeHai
Repo: wled/WLED PR: 4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR `#4798`, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-17T16:32:53.376Z
Learning: In WLED (wled00/FX_fcn.cpp), `setPixelColor()` applies `gamma32()` forward gamma correction at output (line ~1742). User-picked segment colors (`SEGCOLOR(x)`) are stored as raw sRGB values with no gamma transformation in the ingestion path. Since sRGB gamma (~2.2) approximates WLED LED gamma (~2.2-2.8), user-picked colors should be passed to `setPixelColor()` WITHOUT applying `gamma32inv()` first. Applying `gamma32inv` to a user-picked SEGCOLOR before setPixelColor causes over-brightness because it decodes the sRGB encoding and then the output pipeline re-applies gamma, resulting in a net perceptual boost beyond the user's intention.
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-11T20:53:20.707Z
Learning: In WLED, `gamma8inv()` (macro for `NeoGammaWLEDMethod::rawInverseGamma8()`) is an established pattern for pre-compensating values that will later pass through the gamma correction pipeline in `show()`. It is already used in FX_fcn.cpp (Line 1419 for opacity) and FXparticleSystem.cpp (Lines 681, 805, 1534-1535 for pixel brightness). The function is a no-op when `gammaCorrectCol` is false, making it self-consistent. When effects in `wled00/FX.cpp` build colors directly from raw data (e.g., FFT values) and those colors appear too dim in v0.16.0 vs v0.15.0, the fix is to wrap each component in `gamma8inv()` to compensate for the new gamma correction step added in the render path between strip and bus level in v0.16.0.
Learnt from: DedeHai
Repo: wled/WLED PR: 4615
File: wled00/FXparticleSystem.cpp:678-680
Timestamp: 2026-03-29T06:10:40.732Z
Learning: In WLED's FXparticleSystem.cpp, `gamma8inv()` is a lookup-table call that applies the inverse of the gamma curve — it is NOT a no-op (identity function) when `gammaCorrectCol` is false. All calls to `gamma8inv()` on particle brightness values (pxlbrightness[] in renderParticle 2D standard render, the large-particle ellipse path in renderLargeParticle 2D, and pxlbrightness[] in renderParticle 1D) must be guarded by `if (gammaCorrectCol)` to avoid pre-distorting brightness when gamma correction is disabled. Without the guard, particles render noticeably darker/nonlinear in gamma-disabled configurations because the inverse correction is applied but no forward gamma correction follows in show().
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-11T21:43:36.003Z
Learning: In WLED's wled00/FX.cpp, the following effects build colors directly from computed/hardcoded values without using palettes/rainbow/user-defined colors, and have identical color logic in v0.15.0 without gamma8inv. In v0.16.0, these effects need gamma8inv() wrapping to restore previous appearance due to the new render-path gamma correction:
1. mode_traffic_light - hardcoded 0xFF0000, 0xEECC00, 0x00FF00
2. mode_pacifica - hardcoded floor CRGB(2,6,10) and CRGB(2,5,7); overage highlights
3. mode_2Dmatrix - hardcoded default RGBW32(175,255,175,0) and RGBW32(27,130,39,0)
4. mode_2DSunradiation - HeatColor() output goes directly to setPixelColorXY
5. mode_2DFunkyPlank - CHSV(fftResult[band], 255, v) audio-reactive colors
6. mode_waterfall - CHSV(92,92,92) peak indicator (minor)
NOTE: mode_2Ddistortionwaves is NOT a candidate - v0.15.0 applied explicit gamma8() in the effect itself (lines 7581-7583), which was correctly removed in v0.16.0 to rely on render-path gamma instead. Pride2015/ColorWaves already applies gamma32inv() explicitly.
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-17T16:25:46.961Z
Learning: In WLED (wled00/), user-defined segment colors (`SEGCOLOR(x)`, stored in `Segment::colors[]`) are raw sRGB values from the UI color picker — no gamma transformation is applied anywhere in the ingestion path (confirmed in json.cpp and FX_fcn.cpp beginDraw()). However, treating them as "gamma-encoded" requiring `gamma32inv()` before `setPixelColor()` is imprecise: sRGB gamma (~2.2) and WLED's LED gamma (~2.2–2.8) are different curves. From the LED pipeline's perspective, sRGB values are close to linear. The safety of applying `gamma32inv()` to SEGCOLOR values depends on whether WLED's configured LED gamma matches sRGB's gamma exactly. softhack007 confirmed this.
Learnt from: softhack007
Repo: wled/WLED PR: 5635
File: wled00/FX.cpp:2680-2683
Timestamp: 2026-05-20T11:34:59.048Z
Learning: In WLED v16.0.0, colors returned from ColorFromPalette/SEGMENT.color_from_palette are already in linear (gamma-free) space. Do not apply gamma32inv() to palette-derived colors (e.g., Twinklefox twinkle pixels) before brightness calculations; doing so will artificially brighten and can alter hue. Background handling may need gamma8/32 inversions as applicable, but palette colors should be left as-is for linear comparisons.
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-17T16:30:07.598Z
Learning: In WLED 16.0.0 (wled00/FX_fcn.cpp), gamma32() is applied automatically in the output pipeline (Line 1741-1743) to all values stored via setPixelColor(). Therefore, user-picked colors from SEGCOLOR/SEGCOLOR(x) are already sRGB-encoded (gamma ~2.2), and the output gamma32() approximately undoes that encoding, resulting in correct LED luminance. Effects should NOT apply gamma32inv() to SEGCOLOR values before passing to setPixelColor() — doing so would over-brighten them. Only effect-generated linear/perceptual values benefit from gamma32inv() pre-processing (e.g. palette colors, computed brightness values).
Learnt from: DedeHai
Repo: wled/WLED PR: 5601
File: wled00/FX_fcn.cpp:301-323
Timestamp: 2026-05-14T17:13:17.872Z
Learning: In `wled00/FX_fcn.cpp`, inside `Segment::startTransition()`, the `segmentCopy && !_t->_oldSegment` branch intentionally does NOT refresh `_t->_colors`, `_t->_bri`, or `_t->_cct` to the current blended values before restarting the transition timer and creating `_t->_oldSegment`. The `_oldSegment`-based path handles visual continuity differently from the pure FADE path, and this asymmetry is deliberate. Do not flag this as a bug or suggest syncing those fields in that branch.
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-03-25T12:25:11.610Z
Learning: In WLED's `Segment::startTransition()` (wled00/FX_fcn.cpp:284), when `isInTransition()` is true and `_t->_oldSegment` already exists, the function silently returns without updating `_t->_start` or `_t->_bri`. This causes a bug where rapid successive on/off toggles during a non-FADE blending transition (e.g., fairy dust) leave the transition clock stale: by the time of the second re-trigger, elapsed time may already exceed `_dur`, so `updateTransitionProgress()` sets `_progress = 0xFFFF` immediately on the next service tick and `stopTransition()` fires — the blending effect never plays. The fix is to always reset `_t->_start = millis()`, `_t->_dur = dur`, and `_t->_bri = currentBri()` (current visible brightness) in the `isInTransition()` branch, regardless of whether `_oldSegment` exists.
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-11T19:39:20.730Z
Learning: In WLED's `mode_DJLight()` (wled00/FX.cpp), the effect builds color directly as `CRGB(fftResult[15]/2, fftResult[5]/2, fftResult[0]/2)` — plain RGB, not CHSV. The /2 division caps values at 127 max, and with gamma correction enabled this produces very dim output (gamma maps ~50% linear to ~22% perceived brightness). The recommended diagnostic is to uncheck "gamma correction" in LED settings. CHSV→CHSV32 replacement (for 16-bit hue precision) applies to other WLED effects that use CHSV, not DJLight in its current form.
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-12-28T14:06:48.772Z
Learning: In WLED post-commit ee9ac947, the rendering pipeline uses per-segment buffers and per-pixel bus updates. Unmapped (0xFFFF) mapping entries are now skipped in WS2812FX::show() (no “clear to black”), which can leave physical gap LEDs with stale/random colors unless they are explicitly cleared. This is a behavior change from pre-0.16 where a full physical buffer was effectively refreshed each frame.
@softhack007 does it look the same in "peek"? the preview does not have gamma applied and in peek to me I do not see what you describe. So it's either to do with gamma or might be a property of your HUB75 panel
@DedeHai good point, i'll double check 👍 It might even be the HUB75 driver in combination with the 4-scan "outdoor" panel i've used. I have another "indoors" panel, will cross-check with that one, too.
Edit: agreed about the blur statements from the rabbit - what it says is related to "smear blur" only, even then it does not sound very convincing...
@softhack007 does it look the same in "peek"? the preview does not have gamma applied and in peek to me I do not see what you describe. So it's either to do with gamma or might be a property of your HUB75 panel
@DedeHai you're right - in peek preview the black hole "star trails" are still there, just enabling "blur" shortens trails to 2-3 pixels.
So it's either the new HUB75 driver, my panel, or something related to gamma handling.
gamma effectively gets applied twice in the HUB75 path , because we compile without-D NO_CIE1931.
strip -> bus: WLED gamma is applied
HUB75 display->drawPixelRGB888() applies the internal CIE1931 correction
==> two gamma corrections stacked over each other. This perfectly explains that low-brightness trails are not visible. As a cross-check, i've disabled WLED gamma correction, and all pixel trails became visible again.
;; Default I2S mic pins disabled (HUB75 uses GPIOs that would otherwise clash).
I'll try to find a better fix - supporting both NO_CIE1931 and normal HUB75 builds. The idea is that a build with driver CIE enabled can be used together with WLED gamma disabled, for the best possible color accuracy provided at driver level.
Activity
[-]Black hole effect: missing pixel trails in 16.0[/-][+]Black hole effect: missing pixel trails in v16.0[/+]softhack007 commentedon Jun 3, 2026
@coderabbitai please check if the
Segment::blur()andSegment::fade_out()/Segment::fadeToBlackBy()are still producing the same results, considering the different gamma / rendering architectures of 16.0 and 0.15.x.scale(gamma32(pixelColor), brightness)when passing pixels to the driver level, after all effects have calculated their pixels. output drivers do not perform brightness scaling or gamma adjustments.blur()andfade_out()to achieve pixels trails. Pixels from effects are passed on to the driver level; the driver scales brightness but does not re-apply gamma.softhack007 commentedon Jun 3, 2026
@DedeHai i think we've discussed the topic already, just wanted to check if the rabbit comes up with new aspects or consequences that we did not look into yet.
coderabbitai commentedon Jun 3, 2026
🧩 Analysis chain
🏁 Script executed:
Length of output: 1637
🏁 Script executed:
Length of output: 469
🏁 Script executed:
Length of output: 3006
🏁 Script executed:
Length of output: 385
🏁 Script executed:
Length of output: 1025
🏁 Script executed:
Length of output: 5291
🏁 Script executed:
Length of output: 5771
🏁 Script executed:
Length of output: 1269
🏁 Script executed:
Length of output: 4714
@softhack007, great question. After reviewing all three functions and the output pipeline, here is the analysis:Analysis:
blur(),fade_out(),fadeToBlackBy()in v0.16.0 vs v0.15.xAll three operate exclusively on the raw pixel buffer (
getPixelColorRaw/setPixelColorRaw→ directpixels[i]access, no gamma). The critical difference comes from what those pixel values represent and what happens downstream.Pipeline difference recap
gamma32(c)applied, then passes to busvvgamma32(v)≈v^2.8 / 255^1.81.
fadeToBlackBy(fadeBy)— Trails fade faster in v0.16.0gamma32()compresses lower values more aggressively. A pixel at mid-value fades quickly in absolute terms, butgamma32makes the low end of the curve map near-zero faster — trails appear to extinguish sooner for the samefadeByvalue.Net effect: for the same
fadeByparameter, fade trails are visually shorter/dimmer in v0.16.0. Effects with hand-tunedfadeByvalues (e.g. comet trails) will look noticeably different.2.
blur(blur_amount)— Blurs appear slightly brighterblur_amount.Net effect: blur looks more physically correct in v0.16.0 but differs from v0.15.x tuning. Effects that relied on a specific
blur_amountfor their "glow radius" will see it look slightly brighter.3.
fade_out(rate)andfadeToSecondaryBy(fadeBy)— Domain mismatch withcolors[1]This is the most problematic case.
colors[1]isSEGCOLOR(1)— a raw sRGB value from the UI color picker, stored without any gamma transformation. In v0.16.0 the pixel buffer holds linear values, so:colors[1]were approximately gamma-encoded → interpolation was in a consistent domain.colors[1]is sRGB → mixing linear and sRGB values in the same interpolation. The interpolated result will be in neither space correctly. Once outputgamma32()is applied, the secondary color endpoint will be doubly gamma-corrected (once implicitly via sRGB encoding, once at output), making it appear dimmer than intended.Summary table
fadeToBlackBy()fadeByproduces shorter trailsfadeToSecondaryBy()fade_out()blur()blur_amountPotential fixes
fadeToBlackBy()/blur(): These are operating correctly in linear space for v0.16.0. The visual mismatch vs v0.15.x is inherent to the architecture change. Effects relying on specific trail lengths may need their fade/blur parameters retuned.fade_out()/fadeToSecondaryBy(): The secondary color domain mismatch could be corrected by pre-convertingcolors[1]withgamma32inv()before the interpolation, so both endpoints are in linear space beforesetPixelColorRaw:This would make the fade-to-secondary behave consistently with the rest of the linear pipeline.
🧠 Learnings used
softhack007 commentedon Jun 3, 2026
Update: the "Lissajous" effect has similar problems in v16.0: the "Fade Rate" slider has almost zero effect (in 0.15.0 you could smear the curve), and the "Blur" slider just dims the effect down, without much visible blur.
[-]Black hole effect: missing pixel trails in v16.0[/-][+]Black Hole and Lissajous effects: missing pixel trails in v16.0[/+]softhack007 commentedon Jun 3, 2026
"Spaceships" also has similar problems: the "Blur" slider has zero effect.
softhack007 commentedon Jun 3, 2026
@coderabbitai please compare the
Segment::blur()implementation betweenmainand0_15_xbranches. Are there any code changes in the blur() function that could explain the different behaviour?[-]Black Hole and Lissajous effects: missing pixel trails in v16.0[/-][+]Some 2D effects missing pixel trails in v16.0 (Black Hole, Lissajous, Spaceships) [/+]coderabbitai commentedon Jun 3, 2026
🧩 Analysis chain
🏁 Script executed:
Length of output: 5084
🏁 Script executed:
Length of output: 5407
🏁 Script executed:
Length of output: 2982
🏁 Script executed:
Length of output: 6623
🏁 Script executed:
Length of output: 12680
🏁 Script executed:
Length of output: 8400
@softhack007, here is the full comparison. I found three concrete code-level regressions and one architectural change:blur()andblur2D(): main vs 0_15_xBug 1 —
seepformula in smear mode (regression in bothblur()andblur2D())seep = blur_amount >> (1 + smear)blur_amount / 2blur_amount / 4seep = blur_amount >> 1blur_amount / 2blur_amount / 2← doubledWhen
smear=true, v0.16.0 spreads twice as much color into each neighbor compared to v0.15.x. This is a direct regression — effects usingblur(..., smear=true)(e.g. comet trails, Lissajous smear) will look significantly brighter/broader in v0.16.0. The fix:Bug 2 —
color_addpreserveCRflag removedcolor_add(curnew, carryover, true)—preserveCR=true: on overflow, scales result back proportionally to preserve huecolor_add(cur, carryover)—preserveCR=false: per-channel clamp, hue shifts toward white on overflowIn smear mode with bright pixels, v0.15.x preserved hue when neighbor contributions summed above 255; v0.16.0 clips per-channel and can produce white-shifted results.
Architectural change — linear vs gamma-encoded pixel domain
getPixelColor()/setPixelColor()getPixelColorRaw()/setPixelColorRaw()gamma32()applied once at outputBlur arithmetic (
keep * self + seep * neighbors) is physically correct in linear space (v0.16.0), but the gamma curve is very steep at low values. After blurring in linear space,gamma32()at output compresses low-value pixels much harder than v0.15.x did — fading trails extinguish faster for the sameblur_amount. This explains the "dims the effect down" complaint for Lissajous. There is no simple per-call fix for this; effects need theirblur_amountparameters retuned.Change —
blur2D()signatureblur2D(uint8_t blur_amount, bool smear)blur2D(uint8_t blur_x, uint8_t blur_y, bool smear)blur()callsblur2D(blur_amount, blur_amount, smear)symmetrically, so for effects that callblur()directly there is no net difference. The new signature allows asymmetric 2D blurs.Summary
seep = blur_amount >> 1instead of>> (1 + smear)— regressionpreserveCRflag removed fromcolor_addcallsThe
seepformula is an actionable bug fix. The other two are consequences of the deliberate architectural change to linear-space rendering.✏️ Learnings added
🧠 Learnings used
DedeHai commentedon Jun 4, 2026
@softhack007 does it look the same in "peek"? the preview does not have gamma applied and in peek to me I do not see what you describe. So it's either to do with gamma or might be a property of your HUB75 panel
btw the rabbit is wrong about the blur stuff.
softhack007 commentedon Jun 4, 2026
@DedeHai good point, i'll double check 👍 It might even be the HUB75 driver in combination with the 4-scan "outdoor" panel i've used. I have another "indoors" panel, will cross-check with that one, too.
Edit: agreed about the blur statements from the rabbit - what it says is related to "smear blur" only, even then it does not sound very convincing...
DedeHai commentedon Jun 4, 2026
the smear blur in 0.15 is actually changed from my original implementation and was not changed back to "upstream"
459156f
[-]Some 2D effects missing pixel trails in v16.0 (Black Hole, Lissajous, Spaceships) [/-][+]HUB75: Some 2D effects missing pixel trails in v16.0 (Black Hole, Lissajous, Spaceships) [/+]softhack007 commentedon Jun 4, 2026
@DedeHai you're right - in peek preview the black hole "star trails" are still there, just enabling "blur" shortens trails to 2-3 pixels.
So it's either the new HUB75 driver, my panel, or something related to gamma handling.
I'll do some more testing today or tomorrow.
softhack007 commentedon Jun 4, 2026
Update: found one root cause:
gamma effectively gets applied twice in the HUB75 path , because we compile without
-D NO_CIE1931.==> two gamma corrections stacked over each other. This perfectly explains that low-brightness trails are not visible. As a cross-check, i've disabled WLED gamma correction, and all pixel trails became visible again.
The hot fix is to add
-D NO_CIE1931hereWLED/platformio.ini
Lines 730 to 737 in bd0ebf9
I'll try to find a better fix - supporting both NO_CIE1931 and normal HUB75 builds. The idea is that a build with driver CIE enabled can be used together with WLED gamma disabled, for the best possible color accuracy provided at driver level.
--> will add this in PR #5662
HUB75 hotfix: prevent double-applying gamma correction
softhack007 commentedon Jun 16, 2026
solved with #5665
HUB75 hotfix: prevent double-applying gamma correction