perf(grid): lazily allocate rotationTexture; footprint measurement; addresses #338
Safe subset of the #338 drawable memory diet. UIGrid and UIGridView each embedded an sf::RenderTexture rotationTexture value member paid by every grid, though only camera-rotated grids ever use it. Made it a unique_ptr allocated on first rotation, so non-rotating grids (the common case) don't carry it. Measured per-drawable resident cost (issue_338_drawable_footprint_test.py, 5000 instances): Frame ~551 B, Caption ~1135 B (the embedded sf::Text). At hundreds of drawables that is tens of KB total, confirming the issue's own note. The larger render_sprite base-class relocation is therefore DEFERRED -- it would touch every render path, which the issue explicitly says to avoid, for a payoff that does not justify the risk. Left open for that remaining part. Suite 310/310 (rotation tests green through the lazy path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8af3bf45b6
commit
5b2a412c21
5 changed files with 72 additions and 8 deletions
|
|
@ -153,11 +153,12 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
if (has_camera_rotation) {
|
||||
// Ensure rotation texture is large enough
|
||||
unsigned int needed_size = static_cast<unsigned int>(std::max(aabb_w, aabb_h) + 1);
|
||||
if (!rotationTexture) rotationTexture = std::make_unique<sf::RenderTexture>(); // #338
|
||||
if (rotationTextureSize < needed_size) {
|
||||
rotationTexture.create(needed_size, needed_size);
|
||||
rotationTexture->create(needed_size, needed_size);
|
||||
rotationTextureSize = needed_size;
|
||||
}
|
||||
activeTexture = &rotationTexture;
|
||||
activeTexture = rotationTexture.get();
|
||||
activeTexture->clear(fill_color);
|
||||
} else {
|
||||
output.setPosition(box.getPosition() + offset);
|
||||
|
|
@ -385,7 +386,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
renderTexture.clear(fill_color);
|
||||
|
||||
// Create sprite from the larger rotated texture
|
||||
sf::Sprite rotatedSprite(rotationTexture.getTexture());
|
||||
sf::Sprite rotatedSprite(rotationTexture->getTexture());
|
||||
|
||||
// Set origin to center of the rendered content
|
||||
float tex_center_x = aabb_w / 2.0f;
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ public:
|
|||
sf::RenderTexture renderTexture;
|
||||
sf::Vector2u renderTextureSize{0, 0};
|
||||
void ensureRenderTextureSize();
|
||||
sf::RenderTexture rotationTexture;
|
||||
std::unique_ptr<sf::RenderTexture> rotationTexture; // #338 - lazy: only rotating grids allocate it
|
||||
unsigned int rotationTextureSize = 0;
|
||||
|
||||
// Background rendering
|
||||
|
|
|
|||
|
|
@ -192,11 +192,12 @@ void UIGridView::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
|
||||
if (has_camera_rotation) {
|
||||
unsigned int needed_size = static_cast<unsigned int>(std::max(aabb_w, aabb_h) + 1);
|
||||
if (!rotationTexture) rotationTexture = std::make_unique<sf::RenderTexture>(); // #338
|
||||
if (rotationTextureSize < needed_size) {
|
||||
rotationTexture.create(needed_size, needed_size);
|
||||
rotationTexture->create(needed_size, needed_size);
|
||||
rotationTextureSize = needed_size;
|
||||
}
|
||||
activeTexture = &rotationTexture;
|
||||
activeTexture = rotationTexture.get();
|
||||
activeTexture->clear(fill_color);
|
||||
} else {
|
||||
output.setPosition(box.getPosition() + offset);
|
||||
|
|
@ -322,7 +323,7 @@ void UIGridView::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
// Camera rotation compositing
|
||||
if (has_camera_rotation) {
|
||||
renderTexture.clear(fill_color);
|
||||
sf::Sprite rotatedSprite(rotationTexture.getTexture());
|
||||
sf::Sprite rotatedSprite(rotationTexture->getTexture());
|
||||
float tex_center_x = aabb_w / 2.0f;
|
||||
float tex_center_y = aabb_h / 2.0f;
|
||||
rotatedSprite.setOrigin(tex_center_x, tex_center_y);
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ public:
|
|||
sf::RenderTexture renderTexture;
|
||||
sf::Vector2u renderTextureSize{0, 0};
|
||||
void ensureRenderTextureSize();
|
||||
sf::RenderTexture rotationTexture;
|
||||
std::unique_ptr<sf::RenderTexture> rotationTexture; // #338 - lazy: only rotating grids allocate it
|
||||
unsigned int rotationTextureSize = 0;
|
||||
|
||||
// Property system for animations
|
||||
|
|
|
|||
62
tests/regression/issue_338_drawable_footprint_test.py
Normal file
62
tests/regression/issue_338_drawable_footprint_test.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
"""Footprint measurement for #338 - UIDrawable per-instance memory.
|
||||
|
||||
#338 tracks trimming per-drawable memory (the base class is paid by every
|
||||
Frame/Caption/Sprite/Grid). This is the "measure first" deliverable: it reports
|
||||
the approximate per-drawable resident-memory cost (C++ object + Python wrapper +
|
||||
allocator overhead) for a batch of drawables and guards against gross
|
||||
regressions. It is intentionally a loose bound, not a precise sizeof -- the
|
||||
invasive base-class relocations (render_sprite into the cache struct, callback
|
||||
block) are deferred; only the lazy rotationTexture change landed.
|
||||
|
||||
Direct-execution style; Linux RSS via /proc/self/statm.
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
import os
|
||||
|
||||
N = 5000
|
||||
|
||||
|
||||
def rss_bytes():
|
||||
# statm fields are in pages; field[1] is resident set size.
|
||||
with open("/proc/self/statm") as f:
|
||||
pages = int(f.read().split()[1])
|
||||
return pages * os.sysconf("SC_PAGE_SIZE")
|
||||
|
||||
|
||||
def measure(factory, label):
|
||||
base = rss_bytes()
|
||||
keep = [factory(i) for i in range(N)]
|
||||
after = rss_bytes()
|
||||
per = (after - base) / N
|
||||
print(" %-8s %d instances: ~%.0f bytes/instance (RSS delta %.1f MB)"
|
||||
% (label, N, per, (after - base) / 1e6))
|
||||
# Loose guard: catches a gross per-instance blowup, not small drift.
|
||||
assert per < 8192, "%s per-instance RSS %.0f exceeds 8KB guard" % (label, per)
|
||||
return keep
|
||||
|
||||
|
||||
def main():
|
||||
scene = mcrfpy.Scene("t338")
|
||||
|
||||
frames = measure(lambda i: mcrfpy.Frame(pos=(0, 0), size=(4, 4)), "Frame")
|
||||
caps = measure(lambda i: mcrfpy.Caption(pos=(0, 0), text="x"), "Caption")
|
||||
|
||||
# Sanity: a rotating grid still renders correctly after rotationTexture was
|
||||
# made lazily allocated (#338). Just exercise the path; no crash == pass.
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
grid = mcrfpy.Grid(grid_size=(6, 6), texture=tex)
|
||||
scene.children.append(grid)
|
||||
grid.camera_rotation = 30.0
|
||||
from mcrfpy import automation
|
||||
automation.screenshot("/tmp/issue338_rotated.png") # forces render through the lazy path
|
||||
|
||||
# keep references alive until here
|
||||
assert len(frames) == N and len(caps) == N
|
||||
|
||||
print("PASS")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue