Skip to content

propagateSizeToChildren() passes stale m_layout dimensions to Image after dynamic asset injection #96

@unspokenlanguage

Description

@unspokenlanguage

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

  1. 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
  2. Load the .riv file in the C++ runtime
  3. At runtime, inject a new image asset via the ViewModel DataBinding
  4. 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:

  1. syncStyle() runs — correctly updates the Yoga node style to 547×461 fixed dimensions
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions