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
LayoutComponent with fixed dimensions (e.g., 547×461)
- An
Image inside the Layout with Fit set to Contain (or any Fit mode)
- A ViewModel with an Image property bound to the Image component
- Load the
.riv file 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 to 547×461 fixed dimensions
propagateSizeToChildren() runs — reads m_layout.width() and m_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
Summary
When a dynamic image is injected at runtime via DataBinding into an
Imagecomponent inside a fixed-dimensionLayoutComponent, theImagereceives 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
Steps to Reproduce
LayoutComponentwith fixed dimensions (e.g., 547×461)Imageinside the Layout with Fit set to Contain (or any Fit mode).rivfile in the C++ runtimeExpected 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)becausepropagateSizeToChildren()reads fromm_layoutwhich holds stale values from the initial Yoga calculation.Root Cause Analysis
When
Image::setAsset()is called during dynamic injection, it callsmarkParentLayoutDirty(), 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_layoutis a cached result from the previousYGNodeCalculateLayout()pass. At that point, the placeholder image was 32×32, so Yoga computed the layout as 32×32. Even thoughsyncStyle()has now updated the Yoga style to 547×461,calculateLayout()has not re-executed yet, som_layoutstill holds the stale 32×32 values.Timing diagram
Affected Code
File:
src/layout_component.cppFunction:
LayoutComponent::propagateSizeToChildren()Proposed Fix
For
LayoutScaleType::fixedlayouts, the dimensions are deterministic (they are the authoredwidth()andheight()values, independent of Yoga computation). Use these values whenm_layouthasn't caught up:Alternatively, the layout update pipeline could be restructured to ensure
calculateLayout()runs beforepropagateSizeToChildren()within the same update cycle when a layout is marked dirty.Diagnostic Evidence
Before fix —
propagateSizeToChildrenreads stalem_layoutand passes wrong dimensions:After fix —
propagateSizeToChildrenuseseffectivedimensions derived from authoredwidth()/height():