From f490ae18048f3e53ecc3526306bafd93922355fa Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Mon, 5 Oct 2026 16:56:03 -0500 Subject: [PATCH] Keep create_text_sprite text through texture atlas rebuilds create_text_sprite makes an empty texture and draws the text into the atlas. When the atlas fills up after some textures were freed, it rebuilds itself by clearing and redrawing every texture from its image, and a text sprite's image was blank, so the sprite went blank. Copy the drawn pixels back into the texture's image with update_texture_image_from_atlas() after drawing. This also makes sprite.texture.image contain the text. It adds about 0.2-0.6 ms per create_text_sprite call for the GPU read. This caused test_same_text_different_style to fail in CI whenever an earlier test left the shared atlas full enough to rebuild. Add a test that rebuilds the atlas directly. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + arcade/text.py | 4 ++++ tests/unit/text/test_text_sprite.py | 13 +++++++++++++ 3 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9990e3083..7e7145c97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page. - `anchor_x="center"` cut off the left half of the text, and `anchor_x="right"` made an empty sprite. - The sprite was placed too high. It now covers the same area as an `arcade.Text` with the same arguments at the same position. - An empty string raised `ValueError`. It now makes a transparent sprite, as documented. + - Text sprites went blank when the texture atlas rebuilt itself, which happens when it fills up after some textures were freed. The atlas redraws textures from their images, and a text sprite's image was empty. `sprite.texture.image` now also contains the text. ### New Features - Added `arcade.sweep_sprite(sprite, dx, dy, sprite_list)`, which checks the whole path of a moving sprite and returns a `SweepInfo` for the first sprite it would hit (the `sprite`, how far along the move as a `fraction` and `distance`, and the surface `normal`), or `None`. Fast sprites can't pass through thin walls this way. A sprite that already overlaps one is an immediate hit. Added the `sprite_bullets_sweep` example comparing it with a plain collision check. diff --git a/arcade/text.py b/arcade/text.py index 88b083c6f..418970fb5 100644 --- a/arcade/text.py +++ b/arcade/text.py @@ -939,6 +939,10 @@ def create_text_sprite( with texture_atlas.render_into(texture) as fbo: fbo.clear(color=background_color or arcade.color.TRANSPARENT_BLACK) text_object.draw() + # Keep a copy of the pixels in the texture's image. The atlas redraws + # textures from their images when it rebuilds itself, which would + # otherwise leave the sprite blank. + texture_atlas.update_texture_image_from_atlas(texture) # Place the sprite where the Text object was drawn return arcade.Sprite( diff --git a/tests/unit/text/test_text_sprite.py b/tests/unit/text/test_text_sprite.py index 812843f1f..2742df1d3 100644 --- a/tests/unit/text/test_text_sprite.py +++ b/tests/unit/text/test_text_sprite.py @@ -76,6 +76,19 @@ def test_same_text_different_style(window): assert red[1] > 200 and blue[1] == 0 +def test_survives_atlas_rebuild(window): + """The text is kept when the atlas rebuilds itself from the textures' images""" + atlas = window.ctx.default_atlas + sprite = arcade.create_text_sprite("Hello World", color=arcade.color.RED, font_size=20) + image = atlas.read_texture_image_from_atlas(sprite.texture) + assert _ink(image) > 0 + # The texture's own image has the text too + assert sprite.texture.image.tobytes() == image.tobytes() + + atlas.rebuild() + assert atlas.read_texture_image_from_atlas(sprite.texture).tobytes() == image.tobytes() + + def test_empty_text(window): """An empty string makes a transparent sprite instead of failing""" sprite = arcade.create_text_sprite("")