UIDrawable per-instance memory diet (always-embedded render_sprite, duplicate RenderTextures, string members) #338

Open
opened 2026-07-02 12:29:42 +00:00 by john · 1 comment
Owner

From the 2026-07-02 memory-model review. Internal; no API change. Lower priority than the GridData work -- drawable counts are hundreds, not millions -- but the base-class fat is paid by every Frame/Caption/Sprite/Grid.

Findings

  • UIDrawable::render_sprite (sf::Sprite, ~272 bytes) is embedded unconditionally (src/UIDrawable.h:276) but only meaningful when render_texture (already a unique_ptr) exists. Move it into a small heap struct alongside the RenderTexture: unique_ptr<CacheData>{render_texture, render_sprite}.
  • UIGrid carries two sf::Sprite + two sf::RenderTexture (sprite, output, renderTexture, rotationTexture, src/UIGrid.h:75-78); rotationTexture is only needed when rotation is active -- lazy-allocate.
  • Two std::strings per drawable (name, parent_scene) plus 4 unique_ptr mouse-callback slots (src/UIDrawable.h:49-52) -- consider a single lazily-allocated callback block; most drawables have zero handlers.
  • UICaption embeds sf::Text (~280 B), UIFrame embeds sf::RectangleShape (~304 B) -- unavoidable while SFML types are the backing store; note only.

Approach

Measure first: sizeof() report per class in a unit test, then a scene with ~5k drawables before/after (RSS + construction time). Only take changes that don't complicate the render path.

Related: #117 (entity pool -- allocation strategy), #145 (TexturePool -- GPU-side twin of this issue), #255.

From the 2026-07-02 memory-model review. Internal; no API change. Lower priority than the GridData work -- drawable counts are hundreds, not millions -- but the base-class fat is paid by *every* Frame/Caption/Sprite/Grid. ## Findings - `UIDrawable::render_sprite` (`sf::Sprite`, ~272 bytes) is embedded unconditionally (`src/UIDrawable.h:276`) but only meaningful when `render_texture` (already a `unique_ptr`) exists. Move it into a small heap struct alongside the RenderTexture: `unique_ptr<CacheData>{render_texture, render_sprite}`. - `UIGrid` carries **two** `sf::Sprite` + **two** `sf::RenderTexture` (`sprite`, `output`, `renderTexture`, `rotationTexture`, `src/UIGrid.h:75-78`); `rotationTexture` is only needed when rotation is active -- lazy-allocate. - Two `std::string`s per drawable (`name`, `parent_scene`) plus 4 `unique_ptr` mouse-callback slots (`src/UIDrawable.h:49-52`) -- consider a single lazily-allocated callback block; most drawables have zero handlers. - `UICaption` embeds `sf::Text` (~280 B), `UIFrame` embeds `sf::RectangleShape` (~304 B) -- unavoidable while SFML types are the backing store; note only. ## Approach Measure first: `sizeof()` report per class in a unit test, then a scene with ~5k drawables before/after (RSS + construction time). Only take changes that don't complicate the render path. Related: #117 (entity pool -- allocation strategy), #145 (TexturePool -- GPU-side twin of this issue), #255.
Author
Owner

Measured (2026-07-11) via tests/regression/issue_338_drawable_footprint_test.py, 5000 instances, RSS delta / count (C++ object + Python wrapper + allocator overhead):

  • Frame551 B/instance
  • Caption1135 B/instance (the embedded sf::Text)

At the realistic scale this issue itself names (hundreds of drawables, not millions), that's tens of KB total.

Done (commit 5b2a412):

  • UIGrid::rotationTexture and UIGridView::rotationTexture are now lazily-allocated unique_ptrs — only camera-rotated grids allocate the second RenderTexture. Rotation tests green through the lazy path.
  • Added the footprint measurement/guard test.

Deferred (this issue stays open for it):

  • Relocating base-class render_sprite (sf::Sprite, ~272 B) into the RenderTexture cache struct. It's the only meaningful per-drawable win, but it touches every render path — which this issue explicitly says to avoid ("only take changes that don't complicate the render path") — for a payoff of ~tens of KB at real scale. Not worth the render-path risk right now; revisit if a use case pushes drawable counts into the many-thousands or a profiler flags it.
  • The name/parent_scene strings and the 4 callback-slot → single-block consolidation: same cost/benefit calculus, deferred.

Risk budget redirected to the GridData SoA work (#332), where the memory win is 12× and it unblocks numpy views.

**Measured (2026-07-11)** via `tests/regression/issue_338_drawable_footprint_test.py`, 5000 instances, RSS delta / count (C++ object + Python wrapper + allocator overhead): - `Frame` ≈ **551 B/instance** - `Caption` ≈ **1135 B/instance** (the embedded `sf::Text`) At the realistic scale this issue itself names (hundreds of drawables, not millions), that's tens of KB total. **Done** (commit `5b2a412`): - `UIGrid::rotationTexture` and `UIGridView::rotationTexture` are now lazily-allocated `unique_ptr`s — only camera-rotated grids allocate the second RenderTexture. Rotation tests green through the lazy path. - Added the footprint measurement/guard test. **Deferred** (this issue stays open for it): - Relocating base-class `render_sprite` (`sf::Sprite`, ~272 B) into the RenderTexture cache struct. It's the only meaningful per-drawable win, but it touches *every* render path — which this issue explicitly says to avoid ("only take changes that don't complicate the render path") — for a payoff of ~tens of KB at real scale. Not worth the render-path risk right now; revisit if a use case pushes drawable counts into the many-thousands or a profiler flags it. - The `name`/`parent_scene` strings and the 4 callback-slot → single-block consolidation: same cost/benefit calculus, deferred. Risk budget redirected to the GridData SoA work (#332), where the memory win is 12× and it unblocks numpy views.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
john/McRogueFace#338
No description provided.