Repository navigation
propagateSizeToChildren() passes stale m_layout dimensions to Image after dynamic asset injection #96
Description
Summary
When a dynamic image is injected at runtime via DataBinding into an Image component inside a fixed-dimension LayoutComponent, the Image receives stale layout dimensions (from the initial placeholder) instead of the authored Layout dimensions. This causes the image to render at incorrect size despite the Layout having correct fixed dimensions.
Severity
High — Affects all dynamic image injection workflows using DataBinding with Layout containers.
Environment
- Runtime: rive-cpp (C++ runtime)
- Platform: Windows (D3D11 renderer)
- Rive File: Layout (547×461, fixed dimensions) containing an Image with DataBinding ViewModel image property
Steps to Reproduce
- Create a Rive file with:
- An Artboard (1920×1080)
- A
LayoutComponentwith fixed dimensions (e.g., 547×461) - An
Imageinside the Layout with Fit set to Contain (or any Fit mode) - A ViewModel with an Image property bound to the Image component
- Load the
.rivfile in the C++ runtime - At runtime, inject a new image asset via the ViewModel DataBinding
- Observe that the Image renders at ~32×32 (the placeholder size) instead of being fitted to 547×461
Expected Behavior
The Image should receive controlSize(547, 461) and compute its Fit scale relative to the 547×461 Layout container.
Actual Behavior
The Image receives controlSize(32, 32) because propagateSizeToChildren() reads from m_layout which holds stale values from the initial Yoga calculation.
Root Cause Analysis
When Image::setAsset() is called during dynamic injection, it calls markParentLayoutDirty(), which triggers the following update sequence:
syncStyle()runs — correctly updates the Yoga node style to547×461fixed dimensionspropagateSizeToChildren()runs — readsm_layout.width()andm_layout.height()
However, m_layout is a cached result from the previous YGNodeCalculateLayout() pass. At that point, the placeholder image was 32×32, so Yoga computed the layout as 32×32. Even though syncStyle() has now updated the Yoga style to 547×461, calculateLayout() has not re-executed yet, so m_layout still holds the stale 32×32 values.
Timing diagram
Initial load:
syncStyle(547×461 fixed) → calculateLayout() → m_layout = {32×32} ← Yoga sees placeholder
as child content
Dynamic injection:
markParentLayoutDirty()
→ syncStyle(547×461 fixed) ← Updates Yoga style ✓
→ propagateSizeToChildren() ← Reads m_layout = {32×32} ✗ (stale!)
→ Image::controlSize(32, 32) ← Wrong dimensions passed to Image
... calculateLayout() runs later, but propagateSize already fired with wrong values
Affected Code
File: src/layout_component.cpp
Function: LayoutComponent::propagateSizeToChildren()
// Current code reads directly from m_layout (which may be stale):
sizeableChild->controlSize(
Vec2D(m_layout.width(), m_layout.height()), // ← stale cache!
widthScaleType,
heightScaleType,
actualDirection());Proposed Fix
For LayoutScaleType::fixed layouts, the dimensions are deterministic (they are the authored width() and height() values, independent of Yoga computation). Use these values when m_layout hasn't caught up:
float effectiveW = m_layout.width();
float effectiveH = m_layout.height();
if (m_style != nullptr)
{
if (m_style->widthScaleType() == LayoutScaleType::fixed && width() > 0)
{
effectiveW = std::max(effectiveW, width());
}
if (m_style->heightScaleType() == LayoutScaleType::fixed && height() > 0)
{
effectiveH = std::max(effectiveH, height());
}
}
sizeableChild->controlSize(
Vec2D(effectiveW, effectiveH),
widthScaleType,
heightScaleType,
actualDirection());Alternatively, the layout update pipeline could be restructured to ensure calculateLayout() runs before propagateSizeToChildren() within the same update cycle when a layout is marked dirty.
Diagnostic Evidence
Before fix — propagateSizeToChildren reads stale m_layout and passes wrong dimensions:
[RIVE DIAG] syncStyle: w=547.000000 wUnits=1 wScale=0 h=461.000000 hUnits=1 hScale=0 intrinsic=0 leaf=1
[RIVE DIAG] propagateSize: m_layout w=32.000000 h=32.000000
[RIVE DIAG] Image::controlSize called: w=32.000000, h=32.000000
After fix — propagateSizeToChildren uses effective dimensions derived from authored width()/height():
[RIVE DIAG] syncStyle: w=547.000000 wUnits=1 wScale=0 h=461.000000 hUnits=1 hScale=0 intrinsic=0 leaf=1
[RIVE DIAG] propagateSize: m_layout w=32.000000 h=32.000000, effective w=547.000000 h=461.000000, authored w=547.000000 h=461.000000
[RIVE DIAG] propagateSizeToChildren: found child (isSizeable=0)
[RIVE DIAG] propagateSizeToChildren: found child (isSizeable=1)
[RIVE DIAG] Image::controlSize called: w=547.000000, h=461.000000 (prev w=nan, h=nan)
[RIVE DIAG] updateImageScale: renderImage=000001A290F997C0, layoutW=547.000000, layoutH=461.000000, isNanW=0, isNanH=0
[RIVE DIAG] updateImageScale: COMPUTING fit=1, imgW=2160.000000, imgH=3840.000000, layoutW=547.000000, layoutH=461.000000
[RIVE DIAG] updateImageScale: CONTAIN s=0.120052, newScaleX=0.120052, newScaleY=0.120052
Activity