Can a libgdx app swap AssetManager resources and Scene2D skins at runtime without a restart?
0 reputation · 15 Jun 2023, 08:32 UTC
0 reputation · 15 Jun 2023, 08:32 UTC
I am planning a zero-downtime upgrade path for a small libgdx desktop application (currently on 1.11.x, considering 1.12.x). The goal is to ship new textures and a refreshed Scene2D UI skin to a running instance, so users never restart the game loop.
My understanding is that AssetManager supports asynchronous loading and reference-counted unloading, and that a Scene2D Skin can be reloaded while the stage keeps rendering. What is unclear to me is how to sequence the swap safely: when do old assets get disposed if actors still hold TextureRegion references from the previous atlas, and does Skin reload propagate to existing widgets or only to newly created ones?
There is also a threading concern: asset finishLoading callbacks and any UI rebuild would need to happen on the render thread, and I am unsure which parts of the API guarantee that.
Specifically:
32275 reputation · 15 Jun 2023, 15:36 UTC
Yes — a running libgdx app can swap atlases and skins without restarting, but the safe sequence is: load the new assets fully, re-point every actor's style, and only then unload the old atlas. The two traps are disposing too early (one frame of missing textures) and assuming a reloaded Skin propagates to existing widgets (it does not).
assetManager.load("ui-new.atlas", TextureAtlas.class) and load("skin-new.json", Skin.class) (with a SkinParameter pointing at the new atlas if the skin JSON references it).assetManager.update() from your render() method each frame until it returns true for the new assets. Do not touch the stage yet.Skin (or fetch it via assetManager.get()) and walk the stage, reassigning styles: button.setStyle(newSkin.get("default", TextButton.TextButtonStyle.class)), and so on for every widget type you use. Actors that cache TextureRegion or Drawable references directly (custom widgets, Image built from a region) must be given the new regions explicitly or recreated.assetManager.unload("ui-old.atlas"). AssetManager is reference-counted: if the old skin was loaded through the same manager, unloading the skin plus atlas releases the textures once nothing else references them.Because the swap of styles happens inside a single render pass, the user never sees an intermediate frame: the old textures are still alive while the new ones are assigned, and disposal happens afterwards.
This is the confirmed behavior, not a guess: Scene2D widgets copy their style object at construction or setStyle time. Replacing the Skin instance — or even mutating the skin in place — does not update widgets that already hold the old TextButtonStyle, LabelStyle, etc. Every existing widget must be re-styled or rebuilt. Newly created widgets that pull styles from the new skin will look correct automatically, which is why a partial swap often looks like it works until you notice stale screens.
A practical pattern is to keep a registry of style consumers (or rebuild the current screen's root table from a layout method) so the re-skin step is one loop rather than ad-hoc fixes per widget.
AssetManager.load() may be called from any thread, but update() and finishLoading() must run on the render thread — that is where GL texture uploads happen. The style reassignment and stage rebuild must also happen on the render thread, since Scene2D is not thread-safe. If your upgrade arrives via a network callback on another thread, hand it off with Gdx.app.postRunnable() and do the whole swap (steps 3–4) inside that runnable or at the top of the next render().
There are no AssetManager, Skin, or Stage API changes between 1.11.x and 1.12.x that alter this workflow; the same sequence applies to both. (Verify against the 1.12.x changelog before shipping, since this is based on the stable API surface rather than a live check of the latest release.)
unload(), check assetManager.getReferenceCount("ui-old.atlas") is zero and no "texture disposed" warnings appear when old screens are reopened — a stale region reference will surface as a crash or black quad.update() reports completion.Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.