Moved more packing logic to asset thread to prevent ANRs

This commit is contained in:
Anuken 2025-10-27 21:01:10 -04:00
parent a226a74578
commit c06c76d296
4 changed files with 186 additions and 166 deletions

View file

@ -25,6 +25,7 @@ import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.logic.*;
import mindustry.mod.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import mindustry.ui.fragments.*;
@ -147,7 +148,7 @@ public class UI implements ApplicationListener, Loadable{
@Override
public Seq<AssetDescriptor> getDependencies(){
return Seq.with(new AssetDescriptor<>(Control.class), new AssetDescriptor<>("outline", Font.class), new AssetDescriptor<>("default", Font.class));
return Seq.with(new AssetDescriptor<>(Control.class), new AssetDescriptor<>("outline", Font.class), new AssetDescriptor<>("default", Font.class), new AssetDescriptor<>(Mods.class));
}
@Override

View file

@ -59,7 +59,7 @@ public class SectorSubmissions{
registerSerpuloSector(133, "wpx", "https://discord.com/channels/391020510269669376/1379926871227240770/1417920499761156126");
registerSerpuloSector(185, "quad", "https://discord.com/channels/391020510269669376/1379926892181983283/1419231958336016458");
registerSerpuloSector(254, "wpx", "https://discord.com/channels/391020510269669376/1379928045577703424/1420456601667502193");
registerSerpuloSector(0, "Jamespire", "https://discord.com/channels/391020510269669376/1379926780860698784/1418590967384117311");
registerSerpuloSector(0, "iqtik123", "https://discord.com/channels/391020510269669376/1379926780860698784/1431356682834940115");
registerSerpuloSector(103, "enwyz", "https://discord.com/channels/391020510269669376/1379926839559979030/1429203869514207255");
registerSerpuloSector(30, "cyan", "https://discord.com/channels/391020510269669376/1379926800854945823/1423932799647481910");

View file

@ -40,10 +40,6 @@ public class Mods implements Loadable{
private ObjectMap<String, Seq<Fi>> bundles = new ObjectMap<>();
private ObjectSet<String> specialFolders = ObjectSet.with("bundles", "sprites", "sprites-override", ".git");
private int totalSprites;
private ObjectFloatMap<String> textureResize = new ObjectFloatMap<>();
private MultiPacker packer;
/** Ordered mods cache. Set to null to invalidate. */
private @Nullable Seq<LoadedMod> lastOrderedMods = new Seq<>();
@ -144,10 +140,13 @@ public class Mods implements Loadable{
@Override
public void loadAsync(){
if(!mods.contains(LoadedMod::enabled)) return;
Time.mark();
long startTime = Time.millis();
//TODO this should estimate sprite sizes per page
packer = new MultiPacker();
MultiPacker packer = new MultiPacker();
var textureResize = new ObjectFloatMap<String>();
int[] totalSprites = {0};
//all packing tasks to await
var tasks = new Seq<Future<Runnable>>();
@ -155,11 +154,11 @@ public class Mods implements Loadable{
Seq<Fi> sprites = mod.root.child("sprites").findAll(f -> f.extension().equals("png"));
Seq<Fi> overrides = mod.root.child("sprites-override").findAll(f -> f.extension().equals("png"));
packSprites(sprites, mod, true, tasks);
packSprites(overrides, mod, false, tasks);
packSprites(packer, sprites, mod, true, tasks, textureResize);
packSprites(packer, overrides, mod, false, tasks, textureResize);
Log.debug("Packed @ images for mod '@'.", sprites.size + overrides.size, mod.meta.name);
totalSprites += sprites.size + overrides.size;
totalSprites[0] += sprites.size + overrides.size;
});
for(var result : tasks){
@ -179,7 +178,167 @@ public class Mods implements Loadable{
}
}
Log.debug("Time to pack textures: @", Time.elapsed());
Log.debug("Total sprites: @", totalSprites[0]);
TextureFilter filter = Core.settings.getBool("linear", true) ? TextureFilter.linear : TextureFilter.nearest;
Texture[] whiteToDispose = {null};
class RegionEntry{
String name;
PixmapRegion region;
int[] splits, pads;
RegionEntry(String name, PixmapRegion region, int[] splits, int[] pads){
this.name = name;
this.region = region;
this.splits = splits;
this.pads = pads;
}
}
Seq<RegionEntry>[] entries = new Seq[PageType.all.length];
for(int i = 0; i < PageType.all.length; i++){
entries[i] = new Seq<>();
}
ObjectMap<Texture, PageType> pageTypes = ObjectMap.of(
Core.atlas.find("white").texture, PageType.main,
Core.atlas.find("stone1").texture, PageType.environment,
Core.atlas.find("whiteui").texture, PageType.ui,
Core.atlas.find("rubble-1-0").texture, PageType.rubble
);
for(AtlasRegion region : Core.atlas.getRegions()){
PageType type = pageTypes.get(region.texture, PageType.main);
if(!packer.has(type, region.name)){
entries[type.ordinal()].add(new RegionEntry(region.name, Core.atlas.getPixmap(region), region.splits, region.pads));
}
}
//sort each page type by size first, for optimal packing
for(int i = 0; i < PageType.all.length; i++){
var rects = entries[i];
var type = PageType.all[i];
//TODO is this in reverse order?
rects.sort(Structs.comparingInt(o -> -Math.max(o.region.width, o.region.height)));
for(var entry : rects){
packer.add(type, entry.name, entry.region, entry.splits, entry.pads);
}
}
waitForMain(() -> {
Core.atlas.dispose();
//dead shadow-atlas for getting regions, but not pixmaps
var shadow = Core.atlas;
//dummy texture atlas that returns the 'shadow' regions; used for mod loading
Core.atlas = new TextureAtlas(){
boolean foundWhite;
AtlasRegion whiteRegion;
{
//needed for the correct operation of the found() method in the TextureRegion
error = shadow.find("error");
}
@Override
public AtlasRegion white(){
if(Core.app.isOnMainThread() && !foundWhite){
Pixmap pixmap = Pixmaps.blankPixmap();
Texture tex = new Texture(pixmap);
whiteToDispose[0] = tex;
return whiteRegion = new AtlasRegion(tex, 0, 0, 1, 1);
}
return super.white();
}
@Override
public AtlasRegion find(String name){
var base = packer.get(name);
if(base != null){
var reg = new AtlasRegion(shadow.find(name).texture, base.x, base.y, base.width, base.height);
reg.name = name;
reg.pixmapRegion = base;
return reg;
}
return shadow.find(name);
}
@Override
public boolean isFound(TextureRegion region){
return region != shadow.find("error");
}
@Override
public TextureRegion find(String name, TextureRegion def){
return !has(name) ? def : find(name);
}
@Override
public boolean has(String s){
return shadow.has(s) || packer.get(s) != null;
}
//return the *actual* pixmap regions, not the disposed ones.
@Override
public PixmapRegion getPixmap(AtlasRegion region){
PixmapRegion out = packer.get(region.name);
//this should not happen in normal situations
if(out == null) return packer.get("error");
return out;
}
};
});
//generate new icons
for(Seq<Content> arr : content.getContentMap()){
arr.each(c -> {
if(c instanceof UnlockableContent u && c.minfo.mod != null){
u.load();
u.loadIcon();
if(u.generateIcons && !c.minfo.mod.meta.pregenerated){
u.createIcons(packer);
}
}
});
}
waitForMain(() -> {
if(whiteToDispose[0] != null){
whiteToDispose[0].dispose();
}
//replace old atlas data
Core.atlas = packer.flush(filter, new TextureAtlas(){
@Override
public PixmapRegion getPixmap(AtlasRegion region){
var other = super.getPixmap(region);
if(other.pixmap.isDisposed()){
throw new RuntimeException("Calling getPixmap outside of createIcons is not supported!");
}
return other;
}
});
textureResize.each(e -> Core.atlas.find(e.key).scale = e.value);
Core.atlas.setErrorRegion("error");
Log.debug("Total pages: @", Core.atlas.getTextures().size);
packer.printStats();
Events.fire(new AtlasPackEvent());
packer.dispose();
Log.debug("Total time to pack and generate sprites: @ms", Time.timeSinceMillis(startTime));
});
}
private void loadIcons(){
@ -200,7 +359,7 @@ public class Mods implements Loadable{
}
}
private void packSprites(Seq<Fi> sprites, LoadedMod mod, boolean prefix, Seq<Future<Runnable>> tasks){
private void packSprites(MultiPacker packer, Seq<Fi> sprites, LoadedMod mod, boolean prefix, Seq<Future<Runnable>> tasks, ObjectFloatMap<String> textureResize){
boolean bleed = Core.settings.getBool("linear", true) && !mod.meta.pregenerated;
float textureScale = mod.meta.texturescale;
@ -242,162 +401,22 @@ public class Mods implements Loadable{
}
}
void waitForMain(Runnable run){
CountDownLatch latch = new CountDownLatch(1);
Core.app.post(() -> {
run.run();
latch.countDown();
});
try{
latch.await();
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
@Override
public void loadSync(){
loadIcons();
if(packer == null) return;
Time.mark();
//get textures packed
if(totalSprites > 0){
class RegionEntry{
String name;
PixmapRegion region;
int[] splits, pads;
RegionEntry(String name, PixmapRegion region, int[] splits, int[] pads){
this.name = name;
this.region = region;
this.splits = splits;
this.pads = pads;
}
}
Seq<RegionEntry>[] entries = new Seq[PageType.all.length];
for(int i = 0; i < PageType.all.length; i++){
entries[i] = new Seq<>();
}
ObjectMap<Texture, PageType> pageTypes = ObjectMap.of(
Core.atlas.find("white").texture, PageType.main,
Core.atlas.find("stone1").texture, PageType.environment,
Core.atlas.find("whiteui").texture, PageType.ui,
Core.atlas.find("rubble-1-0").texture, PageType.rubble
);
for(AtlasRegion region : Core.atlas.getRegions()){
PageType type = pageTypes.get(region.texture, PageType.main);
if(!packer.has(type, region.name)){
entries[type.ordinal()].add(new RegionEntry(region.name, Core.atlas.getPixmap(region), region.splits, region.pads));
}
}
//sort each page type by size first, for optimal packing
for(int i = 0; i < PageType.all.length; i++){
var rects = entries[i];
var type = PageType.all[i];
//TODO is this in reverse order?
rects.sort(Structs.comparingInt(o -> -Math.max(o.region.width, o.region.height)));
for(var entry : rects){
packer.add(type, entry.name, entry.region, entry.splits, entry.pads);
}
}
Core.atlas.dispose();
//dead shadow-atlas for getting regions, but not pixmaps
var shadow = Core.atlas;
//dummy texture atlas that returns the 'shadow' regions; used for mod loading
Core.atlas = new TextureAtlas(){
{
//needed for the correct operation of the found() method in the TextureRegion
error = shadow.find("error");
}
@Override
public AtlasRegion find(String name){
var base = packer.get(name);
if(base != null){
var reg = new AtlasRegion(shadow.find(name).texture, base.x, base.y, base.width, base.height);
reg.name = name;
reg.pixmapRegion = base;
return reg;
}
return shadow.find(name);
}
@Override
public boolean isFound(TextureRegion region){
return region != shadow.find("error");
}
@Override
public TextureRegion find(String name, TextureRegion def){
return !has(name) ? def : find(name);
}
@Override
public boolean has(String s){
return shadow.has(s) || packer.get(s) != null;
}
//return the *actual* pixmap regions, not the disposed ones.
@Override
public PixmapRegion getPixmap(AtlasRegion region){
PixmapRegion out = packer.get(region.name);
//this should not happen in normal situations
if(out == null) return packer.get("error");
return out;
}
};
TextureFilter filter = Core.settings.getBool("linear", true) ? TextureFilter.linear : TextureFilter.nearest;
Time.mark();
//generate new icons
for(Seq<Content> arr : content.getContentMap()){
arr.each(c -> {
//TODO this can be done in parallel
if(c instanceof UnlockableContent u && c.minfo.mod != null){
u.load();
u.loadIcon();
if(u.generateIcons && !c.minfo.mod.meta.pregenerated){
u.createIcons(packer);
}
}
});
}
Log.debug("Time to generate icons: @", Time.elapsed());
//replace old atlas data
Core.atlas = packer.flush(filter, new TextureAtlas(){
PixmapRegion fake = new PixmapRegion(new Pixmap(1, 1));
boolean didWarn = false;
@Override
public PixmapRegion getPixmap(AtlasRegion region){
var other = super.getPixmap(region);
if(other.pixmap.isDisposed()){
if(!didWarn){
Log.err(new RuntimeException("Calling getPixmap outside of createIcons is not supported! This will be a crash in the future."));
didWarn = true;
}
return fake;
}
return other;
}
});
textureResize.each(e -> Core.atlas.find(e.key).scale = e.value);
Core.atlas.setErrorRegion("error");
Log.debug("Total pages: @", Core.atlas.getTextures().size);
packer.printStats();
Events.fire(new AtlasPackEvent());
}
packer.dispose();
packer = null;
Log.debug("Total time to generate & flush textures synchronously: @", Time.elapsed());
}
private PageType getPage(Fi file){