Fixed text being dropped when the draw context stack overflows - #176

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render
Aug 27, 2026
Merged

Fixed text being dropped when the draw context stack overflows#176
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render

Conversation

@fdesbiens

Copy link
Copy Markdown
Contributor

Problem

guix_all_widgets_accordion_menu has been failing on master with Frame 12 is different since #158.

git bisect run names #158 (8a35f690, the issue #148 stack-corruption fix) as the first bad commit. That commit made every caller of _gx_canvas_drawing_initiate() skip its draw when the call returns GX_DRAW_NESTING_EXCEEDED. For callers that push a nested context on the widget they are already drawing, purely to narrow the clipping rectangle to the client area, that turns a correct rendering into no rendering at all.

The multi-level accordion screen of the all_widgets demo nests widgets nine levels deep — multi_level_accordionmenu_listmla_menu_1_accordionmenu_listtext_view_3 — and _gx_system_canvas_refresh() consumes two contexts before the widget tree is walked (gx_system_canvas_refresh.c:335 and :376). The eight slots of GX_MAX_CONTEXT_NESTING are therefore exhausted before _gx_multi_line_text_view_text_draw() can push its own, and the text is dropped.

The golden file records the correct rendering

Raising GX_MAX_CONTEXT_NESTING in a scratch build makes frame 12 match the existing golden file byte-for-byte. So the golden is the correct output and #158 introduced a visual regression: text GUIX had always drawn is now missing. Regenerating the golden would have blessed that permanently, which is why this PR fixes the code instead and leaves all golden data untouched.

Measured with gdb, one run of the test hits GX_DRAW_NESTING_EXCEEDED 68 times: 67 from _gx_widget_children_draw (not changed by #158 — it is cited there as the reference implementation, and it has always dropped the paint) and exactly 1 from _gx_multi_line_text_view_text_draw. One changed call site, one differing frame.

Fix

#158 conflated two things: "no context was pushed, so _gx_canvas_drawing_complete() must not be called" (correct) with "so nothing may be drawn" (not correct for a clip-only push).

When a widget initiates a nested context on itself just to narrow the clip, the caller's context is still the right context to draw through: it was created for the same widget, carries the same view list, and differs only in gx_draw_context_dirty. These call sites now narrow the caller's clipping rectangle, draw, and restore it:

context=_gx_system_current_draw_context;
saved_dirty=context->gx_draw_context_dirty;
_gx_utility_rectangle_overlap_detect(&saved_dirty, &client, &draw_area);
status=_gx_canvas_drawing_initiate(canvas, (GX_WIDGET*)text_view, &draw_area);
if (status==GX_DRAW_NESTING_EXCEEDED)
{
context->gx_draw_context_dirty=draw_area;
}
if ((status==GX_SUCCESS) || (status==GX_DRAW_NESTING_EXCEEDED))
{
... draw ...
if (status==GX_SUCCESS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
context->gx_draw_context_dirty=saved_dirty;
}
}
elseif (status==GX_NO_VIEWS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
/* GX_INVALID_MEMORY_SIZE and anything else also return before a context is pushed. */
}

_gx_canvas_drawing_complete() is still never called on overflow, so the stack corruption #158 fixed stays fixed. The non-overflow path is unchanged, which is why nothing else in the suite moves.

Applied to the four clip-only call sites:

FileWhy it qualifies
gx_multi_line_text_view_text_draw.cclip-only push on text_view; the reproduced regression
gx_multi_line_text_input_draw.cclip-only push on input
gx_rich_text_view_text_draw.cclip-only push on text_view
gx_single_line_text_input_draw.cclip-only push on widget

Deliberately not changed:

FileWhy not
gx_widget_block_move.ca blit, not a clip refinement; its correct fallback is the dirty list, not a borrowed context
gx_radial_progress_bar_background_draw.cinitiates on a different canvas with who == GX_NULL; there is no caller context on that canvas to borrow

Tests

guix_canvas_draw_nesting_overflow_no_output (added by #158) covers overflow detection and state preservation. Nothing covered what a caller must render — the gap that let this regression through.

New: guix_canvas_draw_nesting_overflow_render_no_output. It fills the draw context stack on a real multi line text view, gives the widget a non-zero whitespace so the narrowed clip is strictly smaller than the widget clip, and asserts that

  1. pixels are written at maximum nesting depth — the draw is not dropped;
  2. the borrowed context comes back with its nesting count, context pointer and all four clip edges unchanged;
  3. the byte checksum of the canvas matches the checksum from the same draw with a nested context available — overflow rendering is identical to normal rendering, not merely non-empty.

The checksum sums raw bytes of gx_canvas_memory, so the test is colour-depth agnostic and behaves the same in every build configuration. Confirmed to fail without the code change:

no pixels were written at maximum draw context nesting depth
Expected: 0x410a9, (266409) Got: 0x0 (0)
Guix Test: guix_canvas_draw_nesting_overflow_render_no_output......Failed!

Three of the four fixed call sites have no existing test that reaches the overflow path, so this is their only coverage.

Results

ConfigurationBeforeAfter
default_build_coverage730/732732/733
partial_canvas_support_build7/77/7
dynamic_bidi_text_build2/32/3, byte-identical output

guix_all_widgets_accordion_menu passes against the unmodified golden file. No golden data was regenerated.

The one remaining default_build_coverage failure, guix_ml_text_view_32bpp, is a separate pre-existing issue with a different cause (no nesting overflow occurs in it at all; #159 word wrapping is the suspect). guix_bidi_text_draw_32bpp in dynamic_bidi_text_build is also pre-existing and produces byte-identical output before and after this change.

Follow-up, not in this PR

The 67 _gx_widget_children_draw overflows are a separate, older defect: the demo's accordion screen needs a peak context depth of 10 (measured by instrumenting _gx_canvas_drawing_initiate()), while GX_MAX_CONTEXT_NESTING defaults to 8. Consequently six frames of guix_all_widgets_accordion_menu's golden data (15–20) record incompletely painted output — they stop matching as soon as the stack is deep enough for the children to be drawn.

Raising the default is well contained (at 12: 304 bytes of static RAM on a 32-bit target, six golden frames to regenerate, and guix_widget_children_draw to rewrite since it exists specifically to exceed the limit), but it changes a public default and checked-in golden data, so it is kept out of this fix.

Closes the regression introduced by #158.

The fix for issue eclipse-threadx#148 (eclipse-threadx#158) made every caller of
_gx_canvas_drawing_initiate() skip its draw when the call returns
GX_DRAW_NESTING_EXCEEDED. Four of those callers push a nested context on the
widget they are already drawing, for one reason only: to narrow the clipping
rectangle to the widget's client area. For them, skipping the draw turns a
correct rendering into no rendering at all, which is what made
guix_all_widgets_accordion_menu report "Frame 12 is different".
The accordion menu screen of the all_widgets demo nests widgets nine levels
deep - multi_level_accordion, menu_list, mla_menu_1_accordion, menu_list,
text_view_3 - and _gx_system_canvas_refresh() consumes two contexts before the
widget tree is walked, so the eight slots of GX_MAX_CONTEXT_NESTING are
exhausted before _gx_multi_line_text_view_text_draw() can push its own. Raising
the limit in a scratch build makes frame 12 match the existing golden file
exactly, which shows that the golden records the correct rendering and that the
text is now being lost rather than merely clipped differently.
When the stack is full there is nothing to push, but the caller's context is
still the right context to draw through: it was created for the same widget and
differs only in its clipping rectangle. These four callers now narrow the
caller's clipping rectangle, draw, and restore it, instead of dropping the draw.
_gx_canvas_drawing_complete() is still not called on overflow, so the stack
corruption that eclipse-threadx#158 fixed stays fixed.
Applied to _gx_multi_line_text_view_text_draw, _gx_multi_line_text_input_draw,
_gx_rich_text_view_text_draw and _gx_single_line_text_input_draw.
_gx_widget_block_move and _gx_radial_progress_bar_background_draw are left
alone: neither pushes a clip-only context on the widget being drawn.
guix_canvas_draw_nesting_overflow_render_no_output closes the coverage gap that
eclipse-threadx#158 left. guix_canvas_draw_nesting_overflow_no_output covers detection and
state preservation; the new test covers what a caller must render: that the text
is still drawn at maximum nesting depth, that the borrowed context is handed
back with its nesting count, context pointer and clipping rectangle unchanged,
and that the pixels match those produced when a nested context is available.
Assisted-by: Claude Code (Opus 5)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fdesbiens
fdesbiens merged commit 3030cc6 into eclipse-threadx:devAug 27, 2026
3 checks passed
@fdesbiens
fdesbiens deleted the fix/draw-nesting-overflow-render branch August 27, 2026 14:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fdesbiens
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fixed text being dropped when the draw context stack overflows - #176

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render
Aug 27, 2026
Merged

Fixed text being dropped when the draw context stack overflows#176
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render

Conversation

@fdesbiens

Copy link
Copy Markdown
Contributor

Problem

guix_all_widgets_accordion_menu has been failing on master with Frame 12 is different since #158.

git bisect run names #158 (8a35f690, the issue #148 stack-corruption fix) as the first bad commit. That commit made every caller of _gx_canvas_drawing_initiate() skip its draw when the call returns GX_DRAW_NESTING_EXCEEDED. For callers that push a nested context on the widget they are already drawing, purely to narrow the clipping rectangle to the client area, that turns a correct rendering into no rendering at all.

The multi-level accordion screen of the all_widgets demo nests widgets nine levels deep — multi_level_accordionmenu_listmla_menu_1_accordionmenu_listtext_view_3 — and _gx_system_canvas_refresh() consumes two contexts before the widget tree is walked (gx_system_canvas_refresh.c:335 and :376). The eight slots of GX_MAX_CONTEXT_NESTING are therefore exhausted before _gx_multi_line_text_view_text_draw() can push its own, and the text is dropped.

The golden file records the correct rendering

Raising GX_MAX_CONTEXT_NESTING in a scratch build makes frame 12 match the existing golden file byte-for-byte. So the golden is the correct output and #158 introduced a visual regression: text GUIX had always drawn is now missing. Regenerating the golden would have blessed that permanently, which is why this PR fixes the code instead and leaves all golden data untouched.

Measured with gdb, one run of the test hits GX_DRAW_NESTING_EXCEEDED 68 times: 67 from _gx_widget_children_draw (not changed by #158 — it is cited there as the reference implementation, and it has always dropped the paint) and exactly 1 from _gx_multi_line_text_view_text_draw. One changed call site, one differing frame.

Fix

#158 conflated two things: "no context was pushed, so _gx_canvas_drawing_complete() must not be called" (correct) with "so nothing may be drawn" (not correct for a clip-only push).

When a widget initiates a nested context on itself just to narrow the clip, the caller's context is still the right context to draw through: it was created for the same widget, carries the same view list, and differs only in gx_draw_context_dirty. These call sites now narrow the caller's clipping rectangle, draw, and restore it:

context=_gx_system_current_draw_context;
saved_dirty=context->gx_draw_context_dirty;
_gx_utility_rectangle_overlap_detect(&saved_dirty, &client, &draw_area);
status=_gx_canvas_drawing_initiate(canvas, (GX_WIDGET*)text_view, &draw_area);
if (status==GX_DRAW_NESTING_EXCEEDED)
{
context->gx_draw_context_dirty=draw_area;
}
if ((status==GX_SUCCESS) || (status==GX_DRAW_NESTING_EXCEEDED))
{
... draw ...
if (status==GX_SUCCESS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
context->gx_draw_context_dirty=saved_dirty;
}
}
elseif (status==GX_NO_VIEWS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
/* GX_INVALID_MEMORY_SIZE and anything else also return before a context is pushed. */
}

_gx_canvas_drawing_complete() is still never called on overflow, so the stack corruption #158 fixed stays fixed. The non-overflow path is unchanged, which is why nothing else in the suite moves.

Applied to the four clip-only call sites:

FileWhy it qualifies
gx_multi_line_text_view_text_draw.cclip-only push on text_view; the reproduced regression
gx_multi_line_text_input_draw.cclip-only push on input
gx_rich_text_view_text_draw.cclip-only push on text_view
gx_single_line_text_input_draw.cclip-only push on widget

Deliberately not changed:

FileWhy not
gx_widget_block_move.ca blit, not a clip refinement; its correct fallback is the dirty list, not a borrowed context
gx_radial_progress_bar_background_draw.cinitiates on a different canvas with who == GX_NULL; there is no caller context on that canvas to borrow

Tests

guix_canvas_draw_nesting_overflow_no_output (added by #158) covers overflow detection and state preservation. Nothing covered what a caller must render — the gap that let this regression through.

New: guix_canvas_draw_nesting_overflow_render_no_output. It fills the draw context stack on a real multi line text view, gives the widget a non-zero whitespace so the narrowed clip is strictly smaller than the widget clip, and asserts that

  1. pixels are written at maximum nesting depth — the draw is not dropped;
  2. the borrowed context comes back with its nesting count, context pointer and all four clip edges unchanged;
  3. the byte checksum of the canvas matches the checksum from the same draw with a nested context available — overflow rendering is identical to normal rendering, not merely non-empty.

The checksum sums raw bytes of gx_canvas_memory, so the test is colour-depth agnostic and behaves the same in every build configuration. Confirmed to fail without the code change:

no pixels were written at maximum draw context nesting depth
Expected: 0x410a9, (266409) Got: 0x0 (0)
Guix Test: guix_canvas_draw_nesting_overflow_render_no_output......Failed!

Three of the four fixed call sites have no existing test that reaches the overflow path, so this is their only coverage.

Results

ConfigurationBeforeAfter
default_build_coverage730/732732/733
partial_canvas_support_build7/77/7
dynamic_bidi_text_build2/32/3, byte-identical output

guix_all_widgets_accordion_menu passes against the unmodified golden file. No golden data was regenerated.

The one remaining default_build_coverage failure, guix_ml_text_view_32bpp, is a separate pre-existing issue with a different cause (no nesting overflow occurs in it at all; #159 word wrapping is the suspect). guix_bidi_text_draw_32bpp in dynamic_bidi_text_build is also pre-existing and produces byte-identical output before and after this change.

Follow-up, not in this PR

The 67 _gx_widget_children_draw overflows are a separate, older defect: the demo's accordion screen needs a peak context depth of 10 (measured by instrumenting _gx_canvas_drawing_initiate()), while GX_MAX_CONTEXT_NESTING defaults to 8. Consequently six frames of guix_all_widgets_accordion_menu's golden data (15–20) record incompletely painted output — they stop matching as soon as the stack is deep enough for the children to be drawn.

Raising the default is well contained (at 12: 304 bytes of static RAM on a 32-bit target, six golden frames to regenerate, and guix_widget_children_draw to rewrite since it exists specifically to exceed the limit), but it changes a public default and checked-in golden data, so it is kept out of this fix.

Closes the regression introduced by #158.

The fix for issue eclipse-threadx#148 (eclipse-threadx#158) made every caller of
_gx_canvas_drawing_initiate() skip its draw when the call returns
GX_DRAW_NESTING_EXCEEDED. Four of those callers push a nested context on the
widget they are already drawing, for one reason only: to narrow the clipping
rectangle to the widget's client area. For them, skipping the draw turns a
correct rendering into no rendering at all, which is what made
guix_all_widgets_accordion_menu report "Frame 12 is different".
The accordion menu screen of the all_widgets demo nests widgets nine levels
deep - multi_level_accordion, menu_list, mla_menu_1_accordion, menu_list,
text_view_3 - and _gx_system_canvas_refresh() consumes two contexts before the
widget tree is walked, so the eight slots of GX_MAX_CONTEXT_NESTING are
exhausted before _gx_multi_line_text_view_text_draw() can push its own. Raising
the limit in a scratch build makes frame 12 match the existing golden file
exactly, which shows that the golden records the correct rendering and that the
text is now being lost rather than merely clipped differently.
When the stack is full there is nothing to push, but the caller's context is
still the right context to draw through: it was created for the same widget and
differs only in its clipping rectangle. These four callers now narrow the
caller's clipping rectangle, draw, and restore it, instead of dropping the draw.
_gx_canvas_drawing_complete() is still not called on overflow, so the stack
corruption that eclipse-threadx#158 fixed stays fixed.
Applied to _gx_multi_line_text_view_text_draw, _gx_multi_line_text_input_draw,
_gx_rich_text_view_text_draw and _gx_single_line_text_input_draw.
_gx_widget_block_move and _gx_radial_progress_bar_background_draw are left
alone: neither pushes a clip-only context on the widget being drawn.
guix_canvas_draw_nesting_overflow_render_no_output closes the coverage gap that
eclipse-threadx#158 left. guix_canvas_draw_nesting_overflow_no_output covers detection and
state preservation; the new test covers what a caller must render: that the text
is still drawn at maximum nesting depth, that the borrowed context is handed
back with its nesting count, context pointer and clipping rectangle unchanged,
and that the pixels match those produced when a nested context is available.
Assisted-by: Claude Code (Opus 5)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fdesbiens
fdesbiens merged commit 3030cc6 into eclipse-threadx:devAug 27, 2026
3 checks passed
@fdesbiens
fdesbiens deleted the fix/draw-nesting-overflow-render branch August 27, 2026 14:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fdesbiens
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fixed text being dropped when the draw context stack overflows - #176

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render
Aug 27, 2026
Merged

Fixed text being dropped when the draw context stack overflows#176
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render

Conversation

@fdesbiens

Copy link
Copy Markdown
Contributor

Problem

guix_all_widgets_accordion_menu has been failing on master with Frame 12 is different since #158.

git bisect run names #158 (8a35f690, the issue #148 stack-corruption fix) as the first bad commit. That commit made every caller of _gx_canvas_drawing_initiate() skip its draw when the call returns GX_DRAW_NESTING_EXCEEDED. For callers that push a nested context on the widget they are already drawing, purely to narrow the clipping rectangle to the client area, that turns a correct rendering into no rendering at all.

The multi-level accordion screen of the all_widgets demo nests widgets nine levels deep — multi_level_accordionmenu_listmla_menu_1_accordionmenu_listtext_view_3 — and _gx_system_canvas_refresh() consumes two contexts before the widget tree is walked (gx_system_canvas_refresh.c:335 and :376). The eight slots of GX_MAX_CONTEXT_NESTING are therefore exhausted before _gx_multi_line_text_view_text_draw() can push its own, and the text is dropped.

The golden file records the correct rendering

Raising GX_MAX_CONTEXT_NESTING in a scratch build makes frame 12 match the existing golden file byte-for-byte. So the golden is the correct output and #158 introduced a visual regression: text GUIX had always drawn is now missing. Regenerating the golden would have blessed that permanently, which is why this PR fixes the code instead and leaves all golden data untouched.

Measured with gdb, one run of the test hits GX_DRAW_NESTING_EXCEEDED 68 times: 67 from _gx_widget_children_draw (not changed by #158 — it is cited there as the reference implementation, and it has always dropped the paint) and exactly 1 from _gx_multi_line_text_view_text_draw. One changed call site, one differing frame.

Fix

#158 conflated two things: "no context was pushed, so _gx_canvas_drawing_complete() must not be called" (correct) with "so nothing may be drawn" (not correct for a clip-only push).

When a widget initiates a nested context on itself just to narrow the clip, the caller's context is still the right context to draw through: it was created for the same widget, carries the same view list, and differs only in gx_draw_context_dirty. These call sites now narrow the caller's clipping rectangle, draw, and restore it:

context=_gx_system_current_draw_context;
saved_dirty=context->gx_draw_context_dirty;
_gx_utility_rectangle_overlap_detect(&saved_dirty, &client, &draw_area);
status=_gx_canvas_drawing_initiate(canvas, (GX_WIDGET*)text_view, &draw_area);
if (status==GX_DRAW_NESTING_EXCEEDED)
{
context->gx_draw_context_dirty=draw_area;
}
if ((status==GX_SUCCESS) || (status==GX_DRAW_NESTING_EXCEEDED))
{
... draw ...
if (status==GX_SUCCESS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
context->gx_draw_context_dirty=saved_dirty;
}
}
elseif (status==GX_NO_VIEWS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
/* GX_INVALID_MEMORY_SIZE and anything else also return before a context is pushed. */
}

_gx_canvas_drawing_complete() is still never called on overflow, so the stack corruption #158 fixed stays fixed. The non-overflow path is unchanged, which is why nothing else in the suite moves.

Applied to the four clip-only call sites:

FileWhy it qualifies
gx_multi_line_text_view_text_draw.cclip-only push on text_view; the reproduced regression
gx_multi_line_text_input_draw.cclip-only push on input
gx_rich_text_view_text_draw.cclip-only push on text_view
gx_single_line_text_input_draw.cclip-only push on widget

Deliberately not changed:

FileWhy not
gx_widget_block_move.ca blit, not a clip refinement; its correct fallback is the dirty list, not a borrowed context
gx_radial_progress_bar_background_draw.cinitiates on a different canvas with who == GX_NULL; there is no caller context on that canvas to borrow

Tests

guix_canvas_draw_nesting_overflow_no_output (added by #158) covers overflow detection and state preservation. Nothing covered what a caller must render — the gap that let this regression through.

New: guix_canvas_draw_nesting_overflow_render_no_output. It fills the draw context stack on a real multi line text view, gives the widget a non-zero whitespace so the narrowed clip is strictly smaller than the widget clip, and asserts that

  1. pixels are written at maximum nesting depth — the draw is not dropped;
  2. the borrowed context comes back with its nesting count, context pointer and all four clip edges unchanged;
  3. the byte checksum of the canvas matches the checksum from the same draw with a nested context available — overflow rendering is identical to normal rendering, not merely non-empty.

The checksum sums raw bytes of gx_canvas_memory, so the test is colour-depth agnostic and behaves the same in every build configuration. Confirmed to fail without the code change:

no pixels were written at maximum draw context nesting depth
Expected: 0x410a9, (266409) Got: 0x0 (0)
Guix Test: guix_canvas_draw_nesting_overflow_render_no_output......Failed!

Three of the four fixed call sites have no existing test that reaches the overflow path, so this is their only coverage.

Results

ConfigurationBeforeAfter
default_build_coverage730/732732/733
partial_canvas_support_build7/77/7
dynamic_bidi_text_build2/32/3, byte-identical output

guix_all_widgets_accordion_menu passes against the unmodified golden file. No golden data was regenerated.

The one remaining default_build_coverage failure, guix_ml_text_view_32bpp, is a separate pre-existing issue with a different cause (no nesting overflow occurs in it at all; #159 word wrapping is the suspect). guix_bidi_text_draw_32bpp in dynamic_bidi_text_build is also pre-existing and produces byte-identical output before and after this change.

Follow-up, not in this PR

The 67 _gx_widget_children_draw overflows are a separate, older defect: the demo's accordion screen needs a peak context depth of 10 (measured by instrumenting _gx_canvas_drawing_initiate()), while GX_MAX_CONTEXT_NESTING defaults to 8. Consequently six frames of guix_all_widgets_accordion_menu's golden data (15–20) record incompletely painted output — they stop matching as soon as the stack is deep enough for the children to be drawn.

Raising the default is well contained (at 12: 304 bytes of static RAM on a 32-bit target, six golden frames to regenerate, and guix_widget_children_draw to rewrite since it exists specifically to exceed the limit), but it changes a public default and checked-in golden data, so it is kept out of this fix.

Closes the regression introduced by #158.

The fix for issue eclipse-threadx#148 (eclipse-threadx#158) made every caller of
_gx_canvas_drawing_initiate() skip its draw when the call returns
GX_DRAW_NESTING_EXCEEDED. Four of those callers push a nested context on the
widget they are already drawing, for one reason only: to narrow the clipping
rectangle to the widget's client area. For them, skipping the draw turns a
correct rendering into no rendering at all, which is what made
guix_all_widgets_accordion_menu report "Frame 12 is different".
The accordion menu screen of the all_widgets demo nests widgets nine levels
deep - multi_level_accordion, menu_list, mla_menu_1_accordion, menu_list,
text_view_3 - and _gx_system_canvas_refresh() consumes two contexts before the
widget tree is walked, so the eight slots of GX_MAX_CONTEXT_NESTING are
exhausted before _gx_multi_line_text_view_text_draw() can push its own. Raising
the limit in a scratch build makes frame 12 match the existing golden file
exactly, which shows that the golden records the correct rendering and that the
text is now being lost rather than merely clipped differently.
When the stack is full there is nothing to push, but the caller's context is
still the right context to draw through: it was created for the same widget and
differs only in its clipping rectangle. These four callers now narrow the
caller's clipping rectangle, draw, and restore it, instead of dropping the draw.
_gx_canvas_drawing_complete() is still not called on overflow, so the stack
corruption that eclipse-threadx#158 fixed stays fixed.
Applied to _gx_multi_line_text_view_text_draw, _gx_multi_line_text_input_draw,
_gx_rich_text_view_text_draw and _gx_single_line_text_input_draw.
_gx_widget_block_move and _gx_radial_progress_bar_background_draw are left
alone: neither pushes a clip-only context on the widget being drawn.
guix_canvas_draw_nesting_overflow_render_no_output closes the coverage gap that
eclipse-threadx#158 left. guix_canvas_draw_nesting_overflow_no_output covers detection and
state preservation; the new test covers what a caller must render: that the text
is still drawn at maximum nesting depth, that the borrowed context is handed
back with its nesting count, context pointer and clipping rectangle unchanged,
and that the pixels match those produced when a nested context is available.
Assisted-by: Claude Code (Opus 5)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fdesbiens
fdesbiens merged commit 3030cc6 into eclipse-threadx:devAug 27, 2026
3 checks passed
@fdesbiens
fdesbiens deleted the fix/draw-nesting-overflow-render branch August 27, 2026 14:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fdesbiens
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fixed text being dropped when the draw context stack overflows - #176

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render
Aug 27, 2026
Merged

Fixed text being dropped when the draw context stack overflows#176
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render

Conversation

@fdesbiens

Copy link
Copy Markdown
Contributor

Problem

guix_all_widgets_accordion_menu has been failing on master with Frame 12 is different since #158.

git bisect run names #158 (8a35f690, the issue #148 stack-corruption fix) as the first bad commit. That commit made every caller of _gx_canvas_drawing_initiate() skip its draw when the call returns GX_DRAW_NESTING_EXCEEDED. For callers that push a nested context on the widget they are already drawing, purely to narrow the clipping rectangle to the client area, that turns a correct rendering into no rendering at all.

The multi-level accordion screen of the all_widgets demo nests widgets nine levels deep — multi_level_accordionmenu_listmla_menu_1_accordionmenu_listtext_view_3 — and _gx_system_canvas_refresh() consumes two contexts before the widget tree is walked (gx_system_canvas_refresh.c:335 and :376). The eight slots of GX_MAX_CONTEXT_NESTING are therefore exhausted before _gx_multi_line_text_view_text_draw() can push its own, and the text is dropped.

The golden file records the correct rendering

Raising GX_MAX_CONTEXT_NESTING in a scratch build makes frame 12 match the existing golden file byte-for-byte. So the golden is the correct output and #158 introduced a visual regression: text GUIX had always drawn is now missing. Regenerating the golden would have blessed that permanently, which is why this PR fixes the code instead and leaves all golden data untouched.

Measured with gdb, one run of the test hits GX_DRAW_NESTING_EXCEEDED 68 times: 67 from _gx_widget_children_draw (not changed by #158 — it is cited there as the reference implementation, and it has always dropped the paint) and exactly 1 from _gx_multi_line_text_view_text_draw. One changed call site, one differing frame.

Fix

#158 conflated two things: "no context was pushed, so _gx_canvas_drawing_complete() must not be called" (correct) with "so nothing may be drawn" (not correct for a clip-only push).

When a widget initiates a nested context on itself just to narrow the clip, the caller's context is still the right context to draw through: it was created for the same widget, carries the same view list, and differs only in gx_draw_context_dirty. These call sites now narrow the caller's clipping rectangle, draw, and restore it:

context=_gx_system_current_draw_context;
saved_dirty=context->gx_draw_context_dirty;
_gx_utility_rectangle_overlap_detect(&saved_dirty, &client, &draw_area);
status=_gx_canvas_drawing_initiate(canvas, (GX_WIDGET*)text_view, &draw_area);
if (status==GX_DRAW_NESTING_EXCEEDED)
{
context->gx_draw_context_dirty=draw_area;
}
if ((status==GX_SUCCESS) || (status==GX_DRAW_NESTING_EXCEEDED))
{
... draw ...
if (status==GX_SUCCESS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
context->gx_draw_context_dirty=saved_dirty;
}
}
elseif (status==GX_NO_VIEWS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
/* GX_INVALID_MEMORY_SIZE and anything else also return before a context is pushed. */
}

_gx_canvas_drawing_complete() is still never called on overflow, so the stack corruption #158 fixed stays fixed. The non-overflow path is unchanged, which is why nothing else in the suite moves.

Applied to the four clip-only call sites:

FileWhy it qualifies
gx_multi_line_text_view_text_draw.cclip-only push on text_view; the reproduced regression
gx_multi_line_text_input_draw.cclip-only push on input
gx_rich_text_view_text_draw.cclip-only push on text_view
gx_single_line_text_input_draw.cclip-only push on widget

Deliberately not changed:

FileWhy not
gx_widget_block_move.ca blit, not a clip refinement; its correct fallback is the dirty list, not a borrowed context
gx_radial_progress_bar_background_draw.cinitiates on a different canvas with who == GX_NULL; there is no caller context on that canvas to borrow

Tests

guix_canvas_draw_nesting_overflow_no_output (added by #158) covers overflow detection and state preservation. Nothing covered what a caller must render — the gap that let this regression through.

New: guix_canvas_draw_nesting_overflow_render_no_output. It fills the draw context stack on a real multi line text view, gives the widget a non-zero whitespace so the narrowed clip is strictly smaller than the widget clip, and asserts that

  1. pixels are written at maximum nesting depth — the draw is not dropped;
  2. the borrowed context comes back with its nesting count, context pointer and all four clip edges unchanged;
  3. the byte checksum of the canvas matches the checksum from the same draw with a nested context available — overflow rendering is identical to normal rendering, not merely non-empty.

The checksum sums raw bytes of gx_canvas_memory, so the test is colour-depth agnostic and behaves the same in every build configuration. Confirmed to fail without the code change:

no pixels were written at maximum draw context nesting depth
Expected: 0x410a9, (266409) Got: 0x0 (0)
Guix Test: guix_canvas_draw_nesting_overflow_render_no_output......Failed!

Three of the four fixed call sites have no existing test that reaches the overflow path, so this is their only coverage.

Results

ConfigurationBeforeAfter
default_build_coverage730/732732/733
partial_canvas_support_build7/77/7
dynamic_bidi_text_build2/32/3, byte-identical output

guix_all_widgets_accordion_menu passes against the unmodified golden file. No golden data was regenerated.

The one remaining default_build_coverage failure, guix_ml_text_view_32bpp, is a separate pre-existing issue with a different cause (no nesting overflow occurs in it at all; #159 word wrapping is the suspect). guix_bidi_text_draw_32bpp in dynamic_bidi_text_build is also pre-existing and produces byte-identical output before and after this change.

Follow-up, not in this PR

The 67 _gx_widget_children_draw overflows are a separate, older defect: the demo's accordion screen needs a peak context depth of 10 (measured by instrumenting _gx_canvas_drawing_initiate()), while GX_MAX_CONTEXT_NESTING defaults to 8. Consequently six frames of guix_all_widgets_accordion_menu's golden data (15–20) record incompletely painted output — they stop matching as soon as the stack is deep enough for the children to be drawn.

Raising the default is well contained (at 12: 304 bytes of static RAM on a 32-bit target, six golden frames to regenerate, and guix_widget_children_draw to rewrite since it exists specifically to exceed the limit), but it changes a public default and checked-in golden data, so it is kept out of this fix.

Closes the regression introduced by #158.

The fix for issue eclipse-threadx#148 (eclipse-threadx#158) made every caller of
_gx_canvas_drawing_initiate() skip its draw when the call returns
GX_DRAW_NESTING_EXCEEDED. Four of those callers push a nested context on the
widget they are already drawing, for one reason only: to narrow the clipping
rectangle to the widget's client area. For them, skipping the draw turns a
correct rendering into no rendering at all, which is what made
guix_all_widgets_accordion_menu report "Frame 12 is different".
The accordion menu screen of the all_widgets demo nests widgets nine levels
deep - multi_level_accordion, menu_list, mla_menu_1_accordion, menu_list,
text_view_3 - and _gx_system_canvas_refresh() consumes two contexts before the
widget tree is walked, so the eight slots of GX_MAX_CONTEXT_NESTING are
exhausted before _gx_multi_line_text_view_text_draw() can push its own. Raising
the limit in a scratch build makes frame 12 match the existing golden file
exactly, which shows that the golden records the correct rendering and that the
text is now being lost rather than merely clipped differently.
When the stack is full there is nothing to push, but the caller's context is
still the right context to draw through: it was created for the same widget and
differs only in its clipping rectangle. These four callers now narrow the
caller's clipping rectangle, draw, and restore it, instead of dropping the draw.
_gx_canvas_drawing_complete() is still not called on overflow, so the stack
corruption that eclipse-threadx#158 fixed stays fixed.
Applied to _gx_multi_line_text_view_text_draw, _gx_multi_line_text_input_draw,
_gx_rich_text_view_text_draw and _gx_single_line_text_input_draw.
_gx_widget_block_move and _gx_radial_progress_bar_background_draw are left
alone: neither pushes a clip-only context on the widget being drawn.
guix_canvas_draw_nesting_overflow_render_no_output closes the coverage gap that
eclipse-threadx#158 left. guix_canvas_draw_nesting_overflow_no_output covers detection and
state preservation; the new test covers what a caller must render: that the text
is still drawn at maximum nesting depth, that the borrowed context is handed
back with its nesting count, context pointer and clipping rectangle unchanged,
and that the pixels match those produced when a nested context is available.
Assisted-by: Claude Code (Opus 5)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fdesbiens
fdesbiens merged commit 3030cc6 into eclipse-threadx:devAug 27, 2026
3 checks passed
@fdesbiens
fdesbiens deleted the fix/draw-nesting-overflow-render branch August 27, 2026 14:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fdesbiens
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fixed text being dropped when the draw context stack overflows - #176

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render
Aug 27, 2026
Merged

Fixed text being dropped when the draw context stack overflows#176
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render

Conversation

@fdesbiens

Copy link
Copy Markdown
Contributor

Problem

guix_all_widgets_accordion_menu has been failing on master with Frame 12 is different since #158.

git bisect run names #158 (8a35f690, the issue #148 stack-corruption fix) as the first bad commit. That commit made every caller of _gx_canvas_drawing_initiate() skip its draw when the call returns GX_DRAW_NESTING_EXCEEDED. For callers that push a nested context on the widget they are already drawing, purely to narrow the clipping rectangle to the client area, that turns a correct rendering into no rendering at all.

The multi-level accordion screen of the all_widgets demo nests widgets nine levels deep — multi_level_accordionmenu_listmla_menu_1_accordionmenu_listtext_view_3 — and _gx_system_canvas_refresh() consumes two contexts before the widget tree is walked (gx_system_canvas_refresh.c:335 and :376). The eight slots of GX_MAX_CONTEXT_NESTING are therefore exhausted before _gx_multi_line_text_view_text_draw() can push its own, and the text is dropped.

The golden file records the correct rendering

Raising GX_MAX_CONTEXT_NESTING in a scratch build makes frame 12 match the existing golden file byte-for-byte. So the golden is the correct output and #158 introduced a visual regression: text GUIX had always drawn is now missing. Regenerating the golden would have blessed that permanently, which is why this PR fixes the code instead and leaves all golden data untouched.

Measured with gdb, one run of the test hits GX_DRAW_NESTING_EXCEEDED 68 times: 67 from _gx_widget_children_draw (not changed by #158 — it is cited there as the reference implementation, and it has always dropped the paint) and exactly 1 from _gx_multi_line_text_view_text_draw. One changed call site, one differing frame.

Fix

#158 conflated two things: "no context was pushed, so _gx_canvas_drawing_complete() must not be called" (correct) with "so nothing may be drawn" (not correct for a clip-only push).

When a widget initiates a nested context on itself just to narrow the clip, the caller's context is still the right context to draw through: it was created for the same widget, carries the same view list, and differs only in gx_draw_context_dirty. These call sites now narrow the caller's clipping rectangle, draw, and restore it:

context=_gx_system_current_draw_context;
saved_dirty=context->gx_draw_context_dirty;
_gx_utility_rectangle_overlap_detect(&saved_dirty, &client, &draw_area);
status=_gx_canvas_drawing_initiate(canvas, (GX_WIDGET*)text_view, &draw_area);
if (status==GX_DRAW_NESTING_EXCEEDED)
{
context->gx_draw_context_dirty=draw_area;
}
if ((status==GX_SUCCESS) || (status==GX_DRAW_NESTING_EXCEEDED))
{
... draw ...
if (status==GX_SUCCESS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
context->gx_draw_context_dirty=saved_dirty;
}
}
elseif (status==GX_NO_VIEWS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
/* GX_INVALID_MEMORY_SIZE and anything else also return before a context is pushed. */
}

_gx_canvas_drawing_complete() is still never called on overflow, so the stack corruption #158 fixed stays fixed. The non-overflow path is unchanged, which is why nothing else in the suite moves.

Applied to the four clip-only call sites:

FileWhy it qualifies
gx_multi_line_text_view_text_draw.cclip-only push on text_view; the reproduced regression
gx_multi_line_text_input_draw.cclip-only push on input
gx_rich_text_view_text_draw.cclip-only push on text_view
gx_single_line_text_input_draw.cclip-only push on widget

Deliberately not changed:

FileWhy not
gx_widget_block_move.ca blit, not a clip refinement; its correct fallback is the dirty list, not a borrowed context
gx_radial_progress_bar_background_draw.cinitiates on a different canvas with who == GX_NULL; there is no caller context on that canvas to borrow

Tests

guix_canvas_draw_nesting_overflow_no_output (added by #158) covers overflow detection and state preservation. Nothing covered what a caller must render — the gap that let this regression through.

New: guix_canvas_draw_nesting_overflow_render_no_output. It fills the draw context stack on a real multi line text view, gives the widget a non-zero whitespace so the narrowed clip is strictly smaller than the widget clip, and asserts that

  1. pixels are written at maximum nesting depth — the draw is not dropped;
  2. the borrowed context comes back with its nesting count, context pointer and all four clip edges unchanged;
  3. the byte checksum of the canvas matches the checksum from the same draw with a nested context available — overflow rendering is identical to normal rendering, not merely non-empty.

The checksum sums raw bytes of gx_canvas_memory, so the test is colour-depth agnostic and behaves the same in every build configuration. Confirmed to fail without the code change:

no pixels were written at maximum draw context nesting depth
Expected: 0x410a9, (266409) Got: 0x0 (0)
Guix Test: guix_canvas_draw_nesting_overflow_render_no_output......Failed!

Three of the four fixed call sites have no existing test that reaches the overflow path, so this is their only coverage.

Results

ConfigurationBeforeAfter
default_build_coverage730/732732/733
partial_canvas_support_build7/77/7
dynamic_bidi_text_build2/32/3, byte-identical output

guix_all_widgets_accordion_menu passes against the unmodified golden file. No golden data was regenerated.

The one remaining default_build_coverage failure, guix_ml_text_view_32bpp, is a separate pre-existing issue with a different cause (no nesting overflow occurs in it at all; #159 word wrapping is the suspect). guix_bidi_text_draw_32bpp in dynamic_bidi_text_build is also pre-existing and produces byte-identical output before and after this change.

Follow-up, not in this PR

The 67 _gx_widget_children_draw overflows are a separate, older defect: the demo's accordion screen needs a peak context depth of 10 (measured by instrumenting _gx_canvas_drawing_initiate()), while GX_MAX_CONTEXT_NESTING defaults to 8. Consequently six frames of guix_all_widgets_accordion_menu's golden data (15–20) record incompletely painted output — they stop matching as soon as the stack is deep enough for the children to be drawn.

Raising the default is well contained (at 12: 304 bytes of static RAM on a 32-bit target, six golden frames to regenerate, and guix_widget_children_draw to rewrite since it exists specifically to exceed the limit), but it changes a public default and checked-in golden data, so it is kept out of this fix.

Closes the regression introduced by #158.

The fix for issue eclipse-threadx#148 (eclipse-threadx#158) made every caller of
_gx_canvas_drawing_initiate() skip its draw when the call returns
GX_DRAW_NESTING_EXCEEDED. Four of those callers push a nested context on the
widget they are already drawing, for one reason only: to narrow the clipping
rectangle to the widget's client area. For them, skipping the draw turns a
correct rendering into no rendering at all, which is what made
guix_all_widgets_accordion_menu report "Frame 12 is different".
The accordion menu screen of the all_widgets demo nests widgets nine levels
deep - multi_level_accordion, menu_list, mla_menu_1_accordion, menu_list,
text_view_3 - and _gx_system_canvas_refresh() consumes two contexts before the
widget tree is walked, so the eight slots of GX_MAX_CONTEXT_NESTING are
exhausted before _gx_multi_line_text_view_text_draw() can push its own. Raising
the limit in a scratch build makes frame 12 match the existing golden file
exactly, which shows that the golden records the correct rendering and that the
text is now being lost rather than merely clipped differently.
When the stack is full there is nothing to push, but the caller's context is
still the right context to draw through: it was created for the same widget and
differs only in its clipping rectangle. These four callers now narrow the
caller's clipping rectangle, draw, and restore it, instead of dropping the draw.
_gx_canvas_drawing_complete() is still not called on overflow, so the stack
corruption that eclipse-threadx#158 fixed stays fixed.
Applied to _gx_multi_line_text_view_text_draw, _gx_multi_line_text_input_draw,
_gx_rich_text_view_text_draw and _gx_single_line_text_input_draw.
_gx_widget_block_move and _gx_radial_progress_bar_background_draw are left
alone: neither pushes a clip-only context on the widget being drawn.
guix_canvas_draw_nesting_overflow_render_no_output closes the coverage gap that
eclipse-threadx#158 left. guix_canvas_draw_nesting_overflow_no_output covers detection and
state preservation; the new test covers what a caller must render: that the text
is still drawn at maximum nesting depth, that the borrowed context is handed
back with its nesting count, context pointer and clipping rectangle unchanged,
and that the pixels match those produced when a nested context is available.
Assisted-by: Claude Code (Opus 5)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fdesbiens
fdesbiens merged commit 3030cc6 into eclipse-threadx:devAug 27, 2026
3 checks passed
@fdesbiens
fdesbiens deleted the fix/draw-nesting-overflow-render branch August 27, 2026 14:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fdesbiens
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fixed text being dropped when the draw context stack overflows - #176

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render
Aug 27, 2026
Merged

Fixed text being dropped when the draw context stack overflows#176
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render

Conversation

@fdesbiens

Copy link
Copy Markdown
Contributor

Problem

guix_all_widgets_accordion_menu has been failing on master with Frame 12 is different since #158.

git bisect run names #158 (8a35f690, the issue #148 stack-corruption fix) as the first bad commit. That commit made every caller of _gx_canvas_drawing_initiate() skip its draw when the call returns GX_DRAW_NESTING_EXCEEDED. For callers that push a nested context on the widget they are already drawing, purely to narrow the clipping rectangle to the client area, that turns a correct rendering into no rendering at all.

The multi-level accordion screen of the all_widgets demo nests widgets nine levels deep — multi_level_accordionmenu_listmla_menu_1_accordionmenu_listtext_view_3 — and _gx_system_canvas_refresh() consumes two contexts before the widget tree is walked (gx_system_canvas_refresh.c:335 and :376). The eight slots of GX_MAX_CONTEXT_NESTING are therefore exhausted before _gx_multi_line_text_view_text_draw() can push its own, and the text is dropped.

The golden file records the correct rendering

Raising GX_MAX_CONTEXT_NESTING in a scratch build makes frame 12 match the existing golden file byte-for-byte. So the golden is the correct output and #158 introduced a visual regression: text GUIX had always drawn is now missing. Regenerating the golden would have blessed that permanently, which is why this PR fixes the code instead and leaves all golden data untouched.

Measured with gdb, one run of the test hits GX_DRAW_NESTING_EXCEEDED 68 times: 67 from _gx_widget_children_draw (not changed by #158 — it is cited there as the reference implementation, and it has always dropped the paint) and exactly 1 from _gx_multi_line_text_view_text_draw. One changed call site, one differing frame.

Fix

#158 conflated two things: "no context was pushed, so _gx_canvas_drawing_complete() must not be called" (correct) with "so nothing may be drawn" (not correct for a clip-only push).

When a widget initiates a nested context on itself just to narrow the clip, the caller's context is still the right context to draw through: it was created for the same widget, carries the same view list, and differs only in gx_draw_context_dirty. These call sites now narrow the caller's clipping rectangle, draw, and restore it:

context=_gx_system_current_draw_context;
saved_dirty=context->gx_draw_context_dirty;
_gx_utility_rectangle_overlap_detect(&saved_dirty, &client, &draw_area);
status=_gx_canvas_drawing_initiate(canvas, (GX_WIDGET*)text_view, &draw_area);
if (status==GX_DRAW_NESTING_EXCEEDED)
{
context->gx_draw_context_dirty=draw_area;
}
if ((status==GX_SUCCESS) || (status==GX_DRAW_NESTING_EXCEEDED))
{
... draw ...
if (status==GX_SUCCESS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
context->gx_draw_context_dirty=saved_dirty;
}
}
elseif (status==GX_NO_VIEWS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
/* GX_INVALID_MEMORY_SIZE and anything else also return before a context is pushed. */
}

_gx_canvas_drawing_complete() is still never called on overflow, so the stack corruption #158 fixed stays fixed. The non-overflow path is unchanged, which is why nothing else in the suite moves.

Applied to the four clip-only call sites:

FileWhy it qualifies
gx_multi_line_text_view_text_draw.cclip-only push on text_view; the reproduced regression
gx_multi_line_text_input_draw.cclip-only push on input
gx_rich_text_view_text_draw.cclip-only push on text_view
gx_single_line_text_input_draw.cclip-only push on widget

Deliberately not changed:

FileWhy not
gx_widget_block_move.ca blit, not a clip refinement; its correct fallback is the dirty list, not a borrowed context
gx_radial_progress_bar_background_draw.cinitiates on a different canvas with who == GX_NULL; there is no caller context on that canvas to borrow

Tests

guix_canvas_draw_nesting_overflow_no_output (added by #158) covers overflow detection and state preservation. Nothing covered what a caller must render — the gap that let this regression through.

New: guix_canvas_draw_nesting_overflow_render_no_output. It fills the draw context stack on a real multi line text view, gives the widget a non-zero whitespace so the narrowed clip is strictly smaller than the widget clip, and asserts that

  1. pixels are written at maximum nesting depth — the draw is not dropped;
  2. the borrowed context comes back with its nesting count, context pointer and all four clip edges unchanged;
  3. the byte checksum of the canvas matches the checksum from the same draw with a nested context available — overflow rendering is identical to normal rendering, not merely non-empty.

The checksum sums raw bytes of gx_canvas_memory, so the test is colour-depth agnostic and behaves the same in every build configuration. Confirmed to fail without the code change:

no pixels were written at maximum draw context nesting depth
Expected: 0x410a9, (266409) Got: 0x0 (0)
Guix Test: guix_canvas_draw_nesting_overflow_render_no_output......Failed!

Three of the four fixed call sites have no existing test that reaches the overflow path, so this is their only coverage.

Results

ConfigurationBeforeAfter
default_build_coverage730/732732/733
partial_canvas_support_build7/77/7
dynamic_bidi_text_build2/32/3, byte-identical output

guix_all_widgets_accordion_menu passes against the unmodified golden file. No golden data was regenerated.

The one remaining default_build_coverage failure, guix_ml_text_view_32bpp, is a separate pre-existing issue with a different cause (no nesting overflow occurs in it at all; #159 word wrapping is the suspect). guix_bidi_text_draw_32bpp in dynamic_bidi_text_build is also pre-existing and produces byte-identical output before and after this change.

Follow-up, not in this PR

The 67 _gx_widget_children_draw overflows are a separate, older defect: the demo's accordion screen needs a peak context depth of 10 (measured by instrumenting _gx_canvas_drawing_initiate()), while GX_MAX_CONTEXT_NESTING defaults to 8. Consequently six frames of guix_all_widgets_accordion_menu's golden data (15–20) record incompletely painted output — they stop matching as soon as the stack is deep enough for the children to be drawn.

Raising the default is well contained (at 12: 304 bytes of static RAM on a 32-bit target, six golden frames to regenerate, and guix_widget_children_draw to rewrite since it exists specifically to exceed the limit), but it changes a public default and checked-in golden data, so it is kept out of this fix.

Closes the regression introduced by #158.

The fix for issue eclipse-threadx#148 (eclipse-threadx#158) made every caller of
_gx_canvas_drawing_initiate() skip its draw when the call returns
GX_DRAW_NESTING_EXCEEDED. Four of those callers push a nested context on the
widget they are already drawing, for one reason only: to narrow the clipping
rectangle to the widget's client area. For them, skipping the draw turns a
correct rendering into no rendering at all, which is what made
guix_all_widgets_accordion_menu report "Frame 12 is different".
The accordion menu screen of the all_widgets demo nests widgets nine levels
deep - multi_level_accordion, menu_list, mla_menu_1_accordion, menu_list,
text_view_3 - and _gx_system_canvas_refresh() consumes two contexts before the
widget tree is walked, so the eight slots of GX_MAX_CONTEXT_NESTING are
exhausted before _gx_multi_line_text_view_text_draw() can push its own. Raising
the limit in a scratch build makes frame 12 match the existing golden file
exactly, which shows that the golden records the correct rendering and that the
text is now being lost rather than merely clipped differently.
When the stack is full there is nothing to push, but the caller's context is
still the right context to draw through: it was created for the same widget and
differs only in its clipping rectangle. These four callers now narrow the
caller's clipping rectangle, draw, and restore it, instead of dropping the draw.
_gx_canvas_drawing_complete() is still not called on overflow, so the stack
corruption that eclipse-threadx#158 fixed stays fixed.
Applied to _gx_multi_line_text_view_text_draw, _gx_multi_line_text_input_draw,
_gx_rich_text_view_text_draw and _gx_single_line_text_input_draw.
_gx_widget_block_move and _gx_radial_progress_bar_background_draw are left
alone: neither pushes a clip-only context on the widget being drawn.
guix_canvas_draw_nesting_overflow_render_no_output closes the coverage gap that
eclipse-threadx#158 left. guix_canvas_draw_nesting_overflow_no_output covers detection and
state preservation; the new test covers what a caller must render: that the text
is still drawn at maximum nesting depth, that the borrowed context is handed
back with its nesting count, context pointer and clipping rectangle unchanged,
and that the pixels match those produced when a nested context is available.
Assisted-by: Claude Code (Opus 5)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fdesbiens
fdesbiens merged commit 3030cc6 into eclipse-threadx:devAug 27, 2026
3 checks passed
@fdesbiens
fdesbiens deleted the fix/draw-nesting-overflow-render branch August 27, 2026 14:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fdesbiens
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fixed text being dropped when the draw context stack overflows - #176

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render
Aug 27, 2026
Merged

Fixed text being dropped when the draw context stack overflows#176
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render

Conversation

@fdesbiens

Copy link
Copy Markdown
Contributor

Problem

guix_all_widgets_accordion_menu has been failing on master with Frame 12 is different since #158.

git bisect run names #158 (8a35f690, the issue #148 stack-corruption fix) as the first bad commit. That commit made every caller of _gx_canvas_drawing_initiate() skip its draw when the call returns GX_DRAW_NESTING_EXCEEDED. For callers that push a nested context on the widget they are already drawing, purely to narrow the clipping rectangle to the client area, that turns a correct rendering into no rendering at all.

The multi-level accordion screen of the all_widgets demo nests widgets nine levels deep — multi_level_accordionmenu_listmla_menu_1_accordionmenu_listtext_view_3 — and _gx_system_canvas_refresh() consumes two contexts before the widget tree is walked (gx_system_canvas_refresh.c:335 and :376). The eight slots of GX_MAX_CONTEXT_NESTING are therefore exhausted before _gx_multi_line_text_view_text_draw() can push its own, and the text is dropped.

The golden file records the correct rendering

Raising GX_MAX_CONTEXT_NESTING in a scratch build makes frame 12 match the existing golden file byte-for-byte. So the golden is the correct output and #158 introduced a visual regression: text GUIX had always drawn is now missing. Regenerating the golden would have blessed that permanently, which is why this PR fixes the code instead and leaves all golden data untouched.

Measured with gdb, one run of the test hits GX_DRAW_NESTING_EXCEEDED 68 times: 67 from _gx_widget_children_draw (not changed by #158 — it is cited there as the reference implementation, and it has always dropped the paint) and exactly 1 from _gx_multi_line_text_view_text_draw. One changed call site, one differing frame.

Fix

#158 conflated two things: "no context was pushed, so _gx_canvas_drawing_complete() must not be called" (correct) with "so nothing may be drawn" (not correct for a clip-only push).

When a widget initiates a nested context on itself just to narrow the clip, the caller's context is still the right context to draw through: it was created for the same widget, carries the same view list, and differs only in gx_draw_context_dirty. These call sites now narrow the caller's clipping rectangle, draw, and restore it:

context=_gx_system_current_draw_context;
saved_dirty=context->gx_draw_context_dirty;
_gx_utility_rectangle_overlap_detect(&saved_dirty, &client, &draw_area);
status=_gx_canvas_drawing_initiate(canvas, (GX_WIDGET*)text_view, &draw_area);
if (status==GX_DRAW_NESTING_EXCEEDED)
{
context->gx_draw_context_dirty=draw_area;
}
if ((status==GX_SUCCESS) || (status==GX_DRAW_NESTING_EXCEEDED))
{
... draw ...
if (status==GX_SUCCESS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
context->gx_draw_context_dirty=saved_dirty;
}
}
elseif (status==GX_NO_VIEWS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
/* GX_INVALID_MEMORY_SIZE and anything else also return before a context is pushed. */
}

_gx_canvas_drawing_complete() is still never called on overflow, so the stack corruption #158 fixed stays fixed. The non-overflow path is unchanged, which is why nothing else in the suite moves.

Applied to the four clip-only call sites:

FileWhy it qualifies
gx_multi_line_text_view_text_draw.cclip-only push on text_view; the reproduced regression
gx_multi_line_text_input_draw.cclip-only push on input
gx_rich_text_view_text_draw.cclip-only push on text_view
gx_single_line_text_input_draw.cclip-only push on widget

Deliberately not changed:

FileWhy not
gx_widget_block_move.ca blit, not a clip refinement; its correct fallback is the dirty list, not a borrowed context
gx_radial_progress_bar_background_draw.cinitiates on a different canvas with who == GX_NULL; there is no caller context on that canvas to borrow

Tests

guix_canvas_draw_nesting_overflow_no_output (added by #158) covers overflow detection and state preservation. Nothing covered what a caller must render — the gap that let this regression through.

New: guix_canvas_draw_nesting_overflow_render_no_output. It fills the draw context stack on a real multi line text view, gives the widget a non-zero whitespace so the narrowed clip is strictly smaller than the widget clip, and asserts that

  1. pixels are written at maximum nesting depth — the draw is not dropped;
  2. the borrowed context comes back with its nesting count, context pointer and all four clip edges unchanged;
  3. the byte checksum of the canvas matches the checksum from the same draw with a nested context available — overflow rendering is identical to normal rendering, not merely non-empty.

The checksum sums raw bytes of gx_canvas_memory, so the test is colour-depth agnostic and behaves the same in every build configuration. Confirmed to fail without the code change:

no pixels were written at maximum draw context nesting depth
Expected: 0x410a9, (266409) Got: 0x0 (0)
Guix Test: guix_canvas_draw_nesting_overflow_render_no_output......Failed!

Three of the four fixed call sites have no existing test that reaches the overflow path, so this is their only coverage.

Results

ConfigurationBeforeAfter
default_build_coverage730/732732/733
partial_canvas_support_build7/77/7
dynamic_bidi_text_build2/32/3, byte-identical output

guix_all_widgets_accordion_menu passes against the unmodified golden file. No golden data was regenerated.

The one remaining default_build_coverage failure, guix_ml_text_view_32bpp, is a separate pre-existing issue with a different cause (no nesting overflow occurs in it at all; #159 word wrapping is the suspect). guix_bidi_text_draw_32bpp in dynamic_bidi_text_build is also pre-existing and produces byte-identical output before and after this change.

Follow-up, not in this PR

The 67 _gx_widget_children_draw overflows are a separate, older defect: the demo's accordion screen needs a peak context depth of 10 (measured by instrumenting _gx_canvas_drawing_initiate()), while GX_MAX_CONTEXT_NESTING defaults to 8. Consequently six frames of guix_all_widgets_accordion_menu's golden data (15–20) record incompletely painted output — they stop matching as soon as the stack is deep enough for the children to be drawn.

Raising the default is well contained (at 12: 304 bytes of static RAM on a 32-bit target, six golden frames to regenerate, and guix_widget_children_draw to rewrite since it exists specifically to exceed the limit), but it changes a public default and checked-in golden data, so it is kept out of this fix.

Closes the regression introduced by #158.

The fix for issue eclipse-threadx#148 (eclipse-threadx#158) made every caller of
_gx_canvas_drawing_initiate() skip its draw when the call returns
GX_DRAW_NESTING_EXCEEDED. Four of those callers push a nested context on the
widget they are already drawing, for one reason only: to narrow the clipping
rectangle to the widget's client area. For them, skipping the draw turns a
correct rendering into no rendering at all, which is what made
guix_all_widgets_accordion_menu report "Frame 12 is different".
The accordion menu screen of the all_widgets demo nests widgets nine levels
deep - multi_level_accordion, menu_list, mla_menu_1_accordion, menu_list,
text_view_3 - and _gx_system_canvas_refresh() consumes two contexts before the
widget tree is walked, so the eight slots of GX_MAX_CONTEXT_NESTING are
exhausted before _gx_multi_line_text_view_text_draw() can push its own. Raising
the limit in a scratch build makes frame 12 match the existing golden file
exactly, which shows that the golden records the correct rendering and that the
text is now being lost rather than merely clipped differently.
When the stack is full there is nothing to push, but the caller's context is
still the right context to draw through: it was created for the same widget and
differs only in its clipping rectangle. These four callers now narrow the
caller's clipping rectangle, draw, and restore it, instead of dropping the draw.
_gx_canvas_drawing_complete() is still not called on overflow, so the stack
corruption that eclipse-threadx#158 fixed stays fixed.
Applied to _gx_multi_line_text_view_text_draw, _gx_multi_line_text_input_draw,
_gx_rich_text_view_text_draw and _gx_single_line_text_input_draw.
_gx_widget_block_move and _gx_radial_progress_bar_background_draw are left
alone: neither pushes a clip-only context on the widget being drawn.
guix_canvas_draw_nesting_overflow_render_no_output closes the coverage gap that
eclipse-threadx#158 left. guix_canvas_draw_nesting_overflow_no_output covers detection and
state preservation; the new test covers what a caller must render: that the text
is still drawn at maximum nesting depth, that the borrowed context is handed
back with its nesting count, context pointer and clipping rectangle unchanged,
and that the pixels match those produced when a nested context is available.
Assisted-by: Claude Code (Opus 5)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fdesbiens
fdesbiens merged commit 3030cc6 into eclipse-threadx:devAug 27, 2026
3 checks passed
@fdesbiens
fdesbiens deleted the fix/draw-nesting-overflow-render branch August 27, 2026 14:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fdesbiens
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fixed text being dropped when the draw context stack overflows - #176

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render
Aug 27, 2026
Merged

Fixed text being dropped when the draw context stack overflows#176
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/draw-nesting-overflow-render

Conversation

@fdesbiens

Copy link
Copy Markdown
Contributor

Problem

guix_all_widgets_accordion_menu has been failing on master with Frame 12 is different since #158.

git bisect run names #158 (8a35f690, the issue #148 stack-corruption fix) as the first bad commit. That commit made every caller of _gx_canvas_drawing_initiate() skip its draw when the call returns GX_DRAW_NESTING_EXCEEDED. For callers that push a nested context on the widget they are already drawing, purely to narrow the clipping rectangle to the client area, that turns a correct rendering into no rendering at all.

The multi-level accordion screen of the all_widgets demo nests widgets nine levels deep — multi_level_accordionmenu_listmla_menu_1_accordionmenu_listtext_view_3 — and _gx_system_canvas_refresh() consumes two contexts before the widget tree is walked (gx_system_canvas_refresh.c:335 and :376). The eight slots of GX_MAX_CONTEXT_NESTING are therefore exhausted before _gx_multi_line_text_view_text_draw() can push its own, and the text is dropped.

The golden file records the correct rendering

Raising GX_MAX_CONTEXT_NESTING in a scratch build makes frame 12 match the existing golden file byte-for-byte. So the golden is the correct output and #158 introduced a visual regression: text GUIX had always drawn is now missing. Regenerating the golden would have blessed that permanently, which is why this PR fixes the code instead and leaves all golden data untouched.

Measured with gdb, one run of the test hits GX_DRAW_NESTING_EXCEEDED 68 times: 67 from _gx_widget_children_draw (not changed by #158 — it is cited there as the reference implementation, and it has always dropped the paint) and exactly 1 from _gx_multi_line_text_view_text_draw. One changed call site, one differing frame.

Fix

#158 conflated two things: "no context was pushed, so _gx_canvas_drawing_complete() must not be called" (correct) with "so nothing may be drawn" (not correct for a clip-only push).

When a widget initiates a nested context on itself just to narrow the clip, the caller's context is still the right context to draw through: it was created for the same widget, carries the same view list, and differs only in gx_draw_context_dirty. These call sites now narrow the caller's clipping rectangle, draw, and restore it:

context=_gx_system_current_draw_context;
saved_dirty=context->gx_draw_context_dirty;
_gx_utility_rectangle_overlap_detect(&saved_dirty, &client, &draw_area);
status=_gx_canvas_drawing_initiate(canvas, (GX_WIDGET*)text_view, &draw_area);
if (status==GX_DRAW_NESTING_EXCEEDED)
{
context->gx_draw_context_dirty=draw_area;
}
if ((status==GX_SUCCESS) || (status==GX_DRAW_NESTING_EXCEEDED))
{
... draw ...
if (status==GX_SUCCESS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
context->gx_draw_context_dirty=saved_dirty;
}
}
elseif (status==GX_NO_VIEWS)
{
_gx_canvas_drawing_complete(canvas, GX_FALSE);
}
else
{
/* GX_INVALID_MEMORY_SIZE and anything else also return before a context is pushed. */
}

_gx_canvas_drawing_complete() is still never called on overflow, so the stack corruption #158 fixed stays fixed. The non-overflow path is unchanged, which is why nothing else in the suite moves.

Applied to the four clip-only call sites:

FileWhy it qualifies
gx_multi_line_text_view_text_draw.cclip-only push on text_view; the reproduced regression
gx_multi_line_text_input_draw.cclip-only push on input
gx_rich_text_view_text_draw.cclip-only push on text_view
gx_single_line_text_input_draw.cclip-only push on widget

Deliberately not changed:

FileWhy not
gx_widget_block_move.ca blit, not a clip refinement; its correct fallback is the dirty list, not a borrowed context
gx_radial_progress_bar_background_draw.cinitiates on a different canvas with who == GX_NULL; there is no caller context on that canvas to borrow

Tests

guix_canvas_draw_nesting_overflow_no_output (added by #158) covers overflow detection and state preservation. Nothing covered what a caller must render — the gap that let this regression through.

New: guix_canvas_draw_nesting_overflow_render_no_output. It fills the draw context stack on a real multi line text view, gives the widget a non-zero whitespace so the narrowed clip is strictly smaller than the widget clip, and asserts that

  1. pixels are written at maximum nesting depth — the draw is not dropped;
  2. the borrowed context comes back with its nesting count, context pointer and all four clip edges unchanged;
  3. the byte checksum of the canvas matches the checksum from the same draw with a nested context available — overflow rendering is identical to normal rendering, not merely non-empty.

The checksum sums raw bytes of gx_canvas_memory, so the test is colour-depth agnostic and behaves the same in every build configuration. Confirmed to fail without the code change:

no pixels were written at maximum draw context nesting depth
Expected: 0x410a9, (266409) Got: 0x0 (0)
Guix Test: guix_canvas_draw_nesting_overflow_render_no_output......Failed!

Three of the four fixed call sites have no existing test that reaches the overflow path, so this is their only coverage.

Results

ConfigurationBeforeAfter
default_build_coverage730/732732/733
partial_canvas_support_build7/77/7
dynamic_bidi_text_build2/32/3, byte-identical output

guix_all_widgets_accordion_menu passes against the unmodified golden file. No golden data was regenerated.

The one remaining default_build_coverage failure, guix_ml_text_view_32bpp, is a separate pre-existing issue with a different cause (no nesting overflow occurs in it at all; #159 word wrapping is the suspect). guix_bidi_text_draw_32bpp in dynamic_bidi_text_build is also pre-existing and produces byte-identical output before and after this change.

Follow-up, not in this PR

The 67 _gx_widget_children_draw overflows are a separate, older defect: the demo's accordion screen needs a peak context depth of 10 (measured by instrumenting _gx_canvas_drawing_initiate()), while GX_MAX_CONTEXT_NESTING defaults to 8. Consequently six frames of guix_all_widgets_accordion_menu's golden data (15–20) record incompletely painted output — they stop matching as soon as the stack is deep enough for the children to be drawn.

Raising the default is well contained (at 12: 304 bytes of static RAM on a 32-bit target, six golden frames to regenerate, and guix_widget_children_draw to rewrite since it exists specifically to exceed the limit), but it changes a public default and checked-in golden data, so it is kept out of this fix.

Closes the regression introduced by #158.

The fix for issue eclipse-threadx#148 (eclipse-threadx#158) made every caller of
_gx_canvas_drawing_initiate() skip its draw when the call returns
GX_DRAW_NESTING_EXCEEDED. Four of those callers push a nested context on the
widget they are already drawing, for one reason only: to narrow the clipping
rectangle to the widget's client area. For them, skipping the draw turns a
correct rendering into no rendering at all, which is what made
guix_all_widgets_accordion_menu report "Frame 12 is different".
The accordion menu screen of the all_widgets demo nests widgets nine levels
deep - multi_level_accordion, menu_list, mla_menu_1_accordion, menu_list,
text_view_3 - and _gx_system_canvas_refresh() consumes two contexts before the
widget tree is walked, so the eight slots of GX_MAX_CONTEXT_NESTING are
exhausted before _gx_multi_line_text_view_text_draw() can push its own. Raising
the limit in a scratch build makes frame 12 match the existing golden file
exactly, which shows that the golden records the correct rendering and that the
text is now being lost rather than merely clipped differently.
When the stack is full there is nothing to push, but the caller's context is
still the right context to draw through: it was created for the same widget and
differs only in its clipping rectangle. These four callers now narrow the
caller's clipping rectangle, draw, and restore it, instead of dropping the draw.
_gx_canvas_drawing_complete() is still not called on overflow, so the stack
corruption that eclipse-threadx#158 fixed stays fixed.
Applied to _gx_multi_line_text_view_text_draw, _gx_multi_line_text_input_draw,
_gx_rich_text_view_text_draw and _gx_single_line_text_input_draw.
_gx_widget_block_move and _gx_radial_progress_bar_background_draw are left
alone: neither pushes a clip-only context on the widget being drawn.
guix_canvas_draw_nesting_overflow_render_no_output closes the coverage gap that
eclipse-threadx#158 left. guix_canvas_draw_nesting_overflow_no_output covers detection and
state preservation; the new test covers what a caller must render: that the text
is still drawn at maximum nesting depth, that the borrowed context is handed
back with its nesting count, context pointer and clipping rectangle unchanged,
and that the pixels match those produced when a nested context is available.
Assisted-by: Claude Code (Opus 5)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fdesbiens
fdesbiens merged commit 3030cc6 into eclipse-threadx:devAug 27, 2026
3 checks passed
@fdesbiens
fdesbiens deleted the fix/draw-nesting-overflow-render branch August 27, 2026 14:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fdesbiens