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.gen.*;
import mindustry.graphics.*; import mindustry.graphics.*;
import mindustry.logic.*; import mindustry.logic.*;
import mindustry.mod.*;
import mindustry.ui.*; import mindustry.ui.*;
import mindustry.ui.dialogs.*; import mindustry.ui.dialogs.*;
import mindustry.ui.fragments.*; import mindustry.ui.fragments.*;
@@ -147,7 +148,7 @@ public class UI implements ApplicationListener, Loadable{
@Override @Override
public Seq<AssetDescriptor> getDependencies(){ 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 @Override

View File

@@ -59,7 +59,7 @@ public class SectorSubmissions{
registerSerpuloSector(133, "wpx", "https://discord.com/channels/391020510269669376/1379926871227240770/1417920499761156126"); registerSerpuloSector(133, "wpx", "https://discord.com/channels/391020510269669376/1379926871227240770/1417920499761156126");
registerSerpuloSector(185, "quad", "https://discord.com/channels/391020510269669376/1379926892181983283/1419231958336016458"); registerSerpuloSector(185, "quad", "https://discord.com/channels/391020510269669376/1379926892181983283/1419231958336016458");
registerSerpuloSector(254, "wpx", "https://discord.com/channels/391020510269669376/1379928045577703424/1420456601667502193"); 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(103, "enwyz", "https://discord.com/channels/391020510269669376/1379926839559979030/1429203869514207255");
registerSerpuloSector(30, "cyan", "https://discord.com/channels/391020510269669376/1379926800854945823/1423932799647481910"); 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 ObjectMap<String, Seq<Fi>> bundles = new ObjectMap<>();
private ObjectSet<String> specialFolders = ObjectSet.with("bundles", "sprites", "sprites-override", ".git"); 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. */ /** Ordered mods cache. Set to null to invalidate. */
private @Nullable Seq<LoadedMod> lastOrderedMods = new Seq<>(); private @Nullable Seq<LoadedMod> lastOrderedMods = new Seq<>();
@@ -144,10 +140,13 @@ public class Mods implements Loadable{
@Override @Override
public void loadAsync(){ public void loadAsync(){
if(!mods.contains(LoadedMod::enabled)) return; if(!mods.contains(LoadedMod::enabled)) return;
Time.mark();
long startTime = Time.millis();
//TODO this should estimate sprite sizes per page //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 //all packing tasks to await
var tasks = new Seq<Future<Runnable>>(); 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> 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")); Seq<Fi> overrides = mod.root.child("sprites-override").findAll(f -> f.extension().equals("png"));
packSprites(sprites, mod, true, tasks); packSprites(packer, sprites, mod, true, tasks, textureResize);
packSprites(overrides, mod, false, tasks); packSprites(packer, overrides, mod, false, tasks, textureResize);
Log.debug("Packed @ images for mod '@'.", sprites.size + overrides.size, mod.meta.name); 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){ for(var result : tasks){
@@ -179,78 +178,10 @@ public class Mods implements Loadable{
} }
} }
Log.debug("Time to pack textures: @", Time.elapsed()); Log.debug("Total sprites: @", totalSprites[0]);
}
private void loadIcons(){ TextureFilter filter = Core.settings.getBool("linear", true) ? TextureFilter.linear : TextureFilter.nearest;
for(LoadedMod mod : mods){ Texture[] whiteToDispose = {null};
loadIcon(mod);
}
}
private void loadIcon(LoadedMod mod){
//try to load icon for each mod that can have one
if(mod.root.child("icon.png").exists() && !headless){
try{
mod.iconTexture = new Texture(mod.root.child("icon.png"));
mod.iconTexture.setFilter(TextureFilter.linear);
}catch(Throwable t){
Log.err("Failed to load icon for mod '" + mod.name + "'.", t);
}
}
}
private void packSprites(Seq<Fi> sprites, LoadedMod mod, boolean prefix, Seq<Future<Runnable>> tasks){
boolean bleed = Core.settings.getBool("linear", true) && !mod.meta.pregenerated;
float textureScale = mod.meta.texturescale;
for(Fi file : sprites){
String
baseName = file.nameWithoutExtension(),
regionName = baseName.contains(".") ? baseName.substring(0, baseName.indexOf(".")) : baseName;
if(!prefix && !Core.atlas.has(regionName)){
Log.warn("Sprite '@' in mod '@' attempts to override a non-existent sprite.", regionName, mod.name);
}
//read and bleed pixmaps in parallel
tasks.add(mainExecutor.submit(() -> {
try{
Pixmap pix = new Pixmap(file.readBytes());
//only bleeds when linear filtering is on at startup
if(bleed){
Pixmaps.bleed(pix, 2);
}
//this returns a *runnable* which actually packs the resulting pixmap; this has to be done synchronously outside the method
return () -> {
//don't prefix with mod name if it's already prefixed by a category, e.g. `block-modname-content-full`.
int hyphen = baseName.indexOf('-');
String fullName = ((prefix && !(hyphen != -1 && baseName.substring(hyphen + 1).startsWith(mod.name + "-"))) ? mod.name + "-" : "") + baseName;
packer.add(getPage(file), fullName, new PixmapRegion(pix));
if(textureScale != 1.0f){
textureResize.put(fullName, textureScale);
}
pix.dispose();
};
}catch(Exception e){
//rethrow exception with details about the cause of failure
throw new Exception("Failed to load image " + file + " for mod " + mod.name, e);
}
}));
}
}
@Override
public void loadSync(){
loadIcons();
if(packer == null) return;
Time.mark();
//get textures packed
if(totalSprites > 0){
class RegionEntry{ class RegionEntry{
String name; String name;
@@ -297,17 +228,32 @@ public class Mods implements Loadable{
} }
} }
waitForMain(() -> {
Core.atlas.dispose(); Core.atlas.dispose();
//dead shadow-atlas for getting regions, but not pixmaps //dead shadow-atlas for getting regions, but not pixmaps
var shadow = Core.atlas; var shadow = Core.atlas;
//dummy texture atlas that returns the 'shadow' regions; used for mod loading //dummy texture atlas that returns the 'shadow' regions; used for mod loading
Core.atlas = new TextureAtlas(){ Core.atlas = new TextureAtlas(){
boolean foundWhite;
AtlasRegion whiteRegion;
{ {
//needed for the correct operation of the found() method in the TextureRegion //needed for the correct operation of the found() method in the TextureRegion
error = shadow.find("error"); 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 @Override
public AtlasRegion find(String name){ public AtlasRegion find(String name){
var base = packer.get(name); var base = packer.get(name);
@@ -346,14 +292,11 @@ public class Mods implements Loadable{
return out; return out;
} }
}; };
});
TextureFilter filter = Core.settings.getBool("linear", true) ? TextureFilter.linear : TextureFilter.nearest;
Time.mark();
//generate new icons //generate new icons
for(Seq<Content> arr : content.getContentMap()){ for(Seq<Content> arr : content.getContentMap()){
arr.each(c -> { arr.each(c -> {
//TODO this can be done in parallel
if(c instanceof UnlockableContent u && c.minfo.mod != null){ if(c instanceof UnlockableContent u && c.minfo.mod != null){
u.load(); u.load();
u.loadIcon(); u.loadIcon();
@@ -363,22 +306,20 @@ public class Mods implements Loadable{
} }
}); });
} }
Log.debug("Time to generate icons: @", Time.elapsed());
waitForMain(() -> {
if(whiteToDispose[0] != null){
whiteToDispose[0].dispose();
}
//replace old atlas data //replace old atlas data
Core.atlas = packer.flush(filter, new TextureAtlas(){ Core.atlas = packer.flush(filter, new TextureAtlas(){
PixmapRegion fake = new PixmapRegion(new Pixmap(1, 1));
boolean didWarn = false;
@Override @Override
public PixmapRegion getPixmap(AtlasRegion region){ public PixmapRegion getPixmap(AtlasRegion region){
var other = super.getPixmap(region); var other = super.getPixmap(region);
if(other.pixmap.isDisposed()){ if(other.pixmap.isDisposed()){
if(!didWarn){ throw new RuntimeException("Calling getPixmap outside of createIcons is not supported!");
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; return other;
@@ -393,11 +334,89 @@ public class Mods implements Loadable{
packer.printStats(); packer.printStats();
Events.fire(new AtlasPackEvent()); Events.fire(new AtlasPackEvent());
}
packer.dispose(); packer.dispose();
packer = null;
Log.debug("Total time to generate & flush textures synchronously: @", Time.elapsed()); Log.debug("Total time to pack and generate sprites: @ms", Time.timeSinceMillis(startTime));
});
}
private void loadIcons(){
for(LoadedMod mod : mods){
loadIcon(mod);
}
}
private void loadIcon(LoadedMod mod){
//try to load icon for each mod that can have one
if(mod.root.child("icon.png").exists() && !headless){
try{
mod.iconTexture = new Texture(mod.root.child("icon.png"));
mod.iconTexture.setFilter(TextureFilter.linear);
}catch(Throwable t){
Log.err("Failed to load icon for mod '" + mod.name + "'.", t);
}
}
}
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;
for(Fi file : sprites){
String
baseName = file.nameWithoutExtension(),
regionName = baseName.contains(".") ? baseName.substring(0, baseName.indexOf(".")) : baseName;
if(!prefix && !Core.atlas.has(regionName)){
Log.warn("Sprite '@' in mod '@' attempts to override a non-existent sprite.", regionName, mod.name);
}
//read and bleed pixmaps in parallel
tasks.add(mainExecutor.submit(() -> {
try{
Pixmap pix = new Pixmap(file.readBytes());
//only bleeds when linear filtering is on at startup
if(bleed){
Pixmaps.bleed(pix, 2);
}
//this returns a *runnable* which actually packs the resulting pixmap; this has to be done synchronously outside the method
return () -> {
//don't prefix with mod name if it's already prefixed by a category, e.g. `block-modname-content-full`.
int hyphen = baseName.indexOf('-');
String fullName = ((prefix && !(hyphen != -1 && baseName.substring(hyphen + 1).startsWith(mod.name + "-"))) ? mod.name + "-" : "") + baseName;
packer.add(getPage(file), fullName, new PixmapRegion(pix));
if(textureScale != 1.0f){
textureResize.put(fullName, textureScale);
}
pix.dispose();
};
}catch(Exception e){
//rethrow exception with details about the cause of failure
throw new Exception("Failed to load image " + file + " for mod " + mod.name, e);
}
}));
}
}
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();
} }
private PageType getPage(Fi file){ private PageType getPage(Fi file){