From 8f52ef56d6f0f23f81a21e7cc7be0999711ca94a Mon Sep 17 00:00:00 2001 From: buthed010203 Date: Tue, 2 Jan 2024 19:41:09 -0500 Subject: [PATCH] - Fix schematic browser lag - Fix schematic browser continuing to consume memory when closed --- core/assets/bundles/bundle.properties | 6 +- core/src/mindustry/client/Main.kt | 2 +- .../mindustry/client/communication/Packets.kt | 6 +- .../ui}/SchematicBrowserDialog.java | 118 +++++++++++++----- core/src/mindustry/core/UI.java | 2 +- core/src/mindustry/game/Schematic.java | 4 +- .../ui/dialogs/SchematicsDialog.java | 24 ++-- gradle.properties | 2 +- 8 files changed, 109 insertions(+), 55 deletions(-) rename core/src/mindustry/{ui/dialogs => client/ui}/SchematicBrowserDialog.java (82%) diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 8ffed33744..1c2c0213aa 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -1443,11 +1443,11 @@ setting.returnonmove.name = Reset Camera On Move setting.returnonmove.description = Stops freecam on player movement as in vanilla setting.nostrafepenalty.name = No strafing speed penalty setting.nostrafepenalty.description = Ignores speed reduction when not moving in the direction your unit is facing (doesn't work on servers) -setting.zerodrift.name = Zero drift -setting.zerodrift.description = Eliminates drifting after releasing movement keys. Useful for precise movement. setting.decreasedrift.name = Decreased Drift setting.decreasedrift.description = Decreases drift on higher speeds. -setting.fastrespawn.name = Fast respawn +setting.zerodrift.name = Zero Drift +setting.zerodrift.description = Eliminates drifting after releasing movement keys. Useful for precise movement. +setting.fastrespawn.name = Fast Respawn setting.fastrespawn.description = Ignores respawn delay and immediately respawns you. setting.graphics.category = Graphics & Appearance diff --git a/core/src/mindustry/client/Main.kt b/core/src/mindustry/client/Main.kt index 9e0a0c1560..8483e2e804 100644 --- a/core/src/mindustry/client/Main.kt +++ b/core/src/mindustry/client/Main.kt @@ -274,7 +274,7 @@ object Main : ApplicationListener { } } - fun send(transmission: Transmission, onFinish: (() -> Unit)? = null) { + fun send(transmission: Transmission, onFinish: Runnable? = null) { communicationClient.send(transmission, onFinish) } diff --git a/core/src/mindustry/client/communication/Packets.kt b/core/src/mindustry/client/communication/Packets.kt index 11ec8eac0f..658499c41d 100644 --- a/core/src/mindustry/client/communication/Packets.kt +++ b/core/src/mindustry/client/communication/Packets.kt @@ -153,7 +153,7 @@ object Packets { val toSend = outgoing.peek() ?: return // Return if there's nothing to send // Gets the next packet in this transmission, if there are no more packets move to the next transmission - val packet = toSend.packets.poll() ?: run { outgoing.remove(toSend); toSend.onFinish?.invoke(); return } + val packet = toSend.packets.poll() ?: run { outgoing.remove(toSend); toSend.onFinish?.run(); return } lastSent.reset(0, 0f) // Sending a packet, reset the timer fully try { communicationSystem.send(packet.bytes()) } catch (e: Exception) { outgoing.remove(toSend); toSend.onError?.invoke() } @@ -213,7 +213,7 @@ object Packets { } catch (e: Exception) { Log.err(e) } } - private data class OutgoingTransmission(val packets: Queue, val onFinish: (() -> Unit)?, val onError: (() -> Unit)?) + private data class OutgoingTransmission(val packets: Queue, val onFinish: Runnable?, val onError: (() -> Unit)?) /** * Splits the transmission into packets and queues them for sending. @@ -221,7 +221,7 @@ object Packets { * @param onFinish A lambda that will be run once it is sent, null by default. * @param onError A lambda that will be run when no suitable message block is found. */ - fun send(transmission: Transmission, onFinish: (() -> Unit)? = null, onError: (() -> Unit)? = null) { + fun send(transmission: Transmission, onFinish: Runnable? = null, onError: (() -> Unit)? = null) { val type = registeredTransmissionTypes.indexOfFirst { it.type == transmission::class } if (transmission.secureOnly && !communicationSystem.secure) throw IllegalArgumentException("Communications system must be secure to send secure-only transmissions!") diff --git a/core/src/mindustry/ui/dialogs/SchematicBrowserDialog.java b/core/src/mindustry/client/ui/SchematicBrowserDialog.java similarity index 82% rename from core/src/mindustry/ui/dialogs/SchematicBrowserDialog.java rename to core/src/mindustry/client/ui/SchematicBrowserDialog.java index 495983010a..a3764a46ab 100644 --- a/core/src/mindustry/ui/dialogs/SchematicBrowserDialog.java +++ b/core/src/mindustry/client/ui/SchematicBrowserDialog.java @@ -1,20 +1,24 @@ -package mindustry.ui.dialogs; +package mindustry.client.ui; import arc.*; import arc.files.*; import arc.graphics.*; +import arc.graphics.gl.*; +import arc.math.geom.*; +import arc.scene.actions.*; import arc.scene.ui.*; import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; -import kotlin.Unit; import mindustry.client.*; import mindustry.client.communication.*; import mindustry.client.navigation.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.type.*; import mindustry.ui.*; +import mindustry.ui.dialogs.*; import java.util.function.*; import java.util.regex.*; @@ -34,16 +38,16 @@ public class SchematicBrowserDialog extends BaseDialog { private Runnable rebuildPane = () -> {}, rebuildTags = () -> {}; private final Pattern ignoreSymbols = Pattern.compile("[`~!@#$%^&*()\\-_=+{}|;:'\",<.>/?]"); private final Seq tags = new Seq<>(), selectedTags = new Seq<>(); + private final ItemSeq reusableItemSeq = new ItemSeq(); public SchematicBrowserDialog(){ super("@schematic.browser"); - Core.assets.load("sprites/schematic-background.png", Texture.class).loaded = t -> t.setWrap(Texture.TextureWrap.repeat); shouldPause = true; addCloseButton(); - buttons.button("@schematic", Icon.copy, this::hideBrowser); - buttons.button("@schematic.browser.repo", Icon.host, this.repositoriesDialog::show); - buttons.button("@schematic.browser.fetch", Icon.refresh, () -> fetch(loadedRepositories.keys().toSeq())); + buttons.button("@schematics", Icon.copy, this::hideBrowser); + buttons.button("@schematic.browser.repo", Icon.host, repositoriesDialog::show); + buttons.button("@schematic.browser.fetch", Icon.refresh, () -> fetch(repositoryLinks)); makeButtonOverlay(); getSettings(); @@ -52,9 +56,36 @@ public SchematicBrowserDialog(){ shown(this::setup); onResize(this::setup); + + setHideAction(() -> Actions.run(() -> { // Nuke previews to save ram FINISHME: Nuke the schematics as well and reload them on dialog open. Ideally, we should do that across all threads similar to how we load saves + var previews = Reflect.>get(schematics, "previews"); + var removed = new Queue(); + for (var schems : loadedRepositories.values()) { + for (var schem : schems) { + var rem = previews.remove(schem); + if (rem != null) removed.add(rem); + } + } + Core.app.post(() -> disposeBuffers(removed)); // Start removing next frame as the process above may already take a few msec on slow cpus or in large repositories + })); + } + + /** Disposes a list of FrameBuffers over the course of multiple frames to not cause lag. */ + void disposeBuffers(Queue todo) { + var start = Time.nanos(); + while (!todo.isEmpty()) { + if (Time.millisSinceNanos(start) >= 5) { + Log.debug("Couldn't finish disposing buffers in time, resuming next frame. @ remain", todo.size); + Core.app.post(() -> disposeBuffers(todo)); + return; + } + todo.removeFirst().dispose(); + } + Log.debug("Finished disposing of FrameBuffers"); } void setup(){ + Time.mark(); search = ""; cont.top(); @@ -100,18 +131,30 @@ void setup(){ }).height(tagh).fillX(); cont.row(); - cont.pane(t -> { - t.top(); - rebuildPane = () -> { - t.clear(); - firstSchematic = null; - for (String repo : loadedRepositories.keys()) { - if (hiddenRepositories.contains(repo)) continue; - setupRepoUi(t, ignoreSymbols.matcher(search.toLowerCase()).replaceAll(""), repo); - } - }; - rebuildPane.run(); - }).grow().scrollX(false); + Table[] t = {null}; // Peak java + t[0] = new Table() { + @Override + public void setCullingArea(Rect cullingArea) { + super.setCullingArea(cullingArea); + t[0].getChildren().each(c -> c instanceof Table, c -> { + var area = t[0].getCullingArea(); + c.getCullingArea().setSize(area.width, area.height) // Set the size (NOT scaled to child coordinates which it should be if either scale isn't 1) + .setPosition(c.parentToLocalCoordinates(area.getPosition(Tmp.v1))); // Set the position (scaled correctly) + }); + } + }; + t[0].top(); + rebuildPane = () -> { + t[0].clear(); + firstSchematic = null; + for (String repo : loadedRepositories.keys()) { + if (hiddenRepositories.contains(repo)) continue; + setupRepoUi(t[0], ignoreSymbols.matcher(search.toLowerCase()).replaceAll(""), repo); + } + }; + rebuildPane.run(); + cont.pane(t[0]).grow().scrollX(false); + Log.info("Rebuilt Schematic Browser in @ms", Time.elapsed()); } void setupRepoUi(Table table, String searchString, String repo){ @@ -122,10 +165,12 @@ void setupRepoUi(Table table, String searchString, String repo){ table.image().growX().padTop(10).height(3).color(Pal.accent).center(); table.row(); table.table(t -> { - int i = 0; + t.setCullingArea(new Rect()); // Make sure this isn't null for later + + int[] i = {0}; final int max = Core.settings.getInt("maxschematicslisted"); for(Schematic s : loadedRepositories.get(repo)){ - if(max != 0 && i > max) break; // Avoid meltdown on large repositories + if(max != 0 && i[0] > max) break; // Avoid meltdown on large repositories if(selectedTags.any() && !s.labels.containsAll(selectedTags)) continue; // Tags if(!search.isEmpty() && !(ignoreSymbols.matcher(s.name().toLowerCase()).replaceAll("").contains(searchString) @@ -148,15 +193,24 @@ void setupRepoUi(Table table, String searchString, String repo){ buttons.button(Icon.download, style, () -> { ui.showInfoFade("@schematic.saved"); schematics.add(s); - ui.schematics.checkTags(s); + Reflect.invoke(ui.schematics, "checkTags", new Object[]{s}, Schematic.class); // Vars.ui.schematics.checkTags(s) }).tooltip("@schematic.browser.download"); }).growX().height(50f); b.row(); b.stack(new SchematicsDialog.SchematicImage(s).setScaling(Scaling.fit), new Table(n -> { n.top(); n.table(Styles.black3, c -> { - Label label = c.add(s.name()).style(Styles.outlineLabel).top().growX().maxWidth(200f - 8f) - .update(l -> l.setText((!player.team().rules().infiniteResources && !state.rules.infiniteResources && player.core() != null && !player.core().items.has(s.requirements()) ? "[#dd5656]" : "") + s.name())).get(); + Label label = c.add("").style(Styles.outlineLabel).top().growX().maxWidth(200f - 8f) + .update(l -> { + var txt = l.getText(); // Update the stringBuilder directly + if (txt.length() == 0 || (Core.graphics.getFrameId() + i[0]) % 60 == 0) { // update() is run every frame even when the element is culled out, the solution is to only update a portion every frame FINISHME: Do we want to hack this and update the text in the draw() method which is only called when the element isn't culled? + txt.setLength(0); + if (!player.team().rules().infiniteResources && !state.rules.infiniteResources && player.core() != null && !player.core().items.has(s.requirements(reusableItemSeq))) txt.append("[#dd5656]"); + txt.append(s.name()); + reusableItemSeq.clear(); + } + }).get(); + label.runUpdate(); // Update the text instantly label.setEllipsis(true); label.setAlignment(Align.center); }).growX().margin(1).pad(4).maxWidth(Scl.scl(200f - 8f)).padBottom(0); @@ -177,12 +231,12 @@ void setupRepoUi(Table table, String searchString, String repo){ sel[0].getStyle().up = Tex.pane; - if(++i % cols == 0){ + if(++i[0] % cols == 0){ t.row(); } } - if(i==0){ + if(i[0]==0){ if(!searchString.isEmpty() || selectedTags.any()){ t.add("@none.found"); }else{ @@ -224,10 +278,9 @@ public void showExport(Schematic s){ t.button("@schematic.chatshare", Icon.bookOpen, style, () -> { if (!state.isPlaying()) return; dialog.hide(); - clientThread.post(() -> Main.INSTANCE.send(new SchematicTransmission(s), () -> { - Core.app.post(() -> ui.showInfoToast(Core.bundle.get("client.finisheduploading"), 2f)); - return Unit.INSTANCE; - })); + clientThread.post(() -> Main.INSTANCE.send(new SchematicTransmission(s), () -> Core.app.post(() -> + ui.showInfoToast(Core.bundle.get("client.finisheduploading"), 2f) + ))); }).marginLeft(12f).get().setDisabled(() -> !state.isPlaying()); }); }); @@ -394,7 +447,7 @@ void showAllTags(){ void hideBrowser(){ ui.schematics.show(); - this.hide(); + hide(); } void getSettings(){ @@ -436,6 +489,7 @@ void loadRepositories(){ } void fetch(Seq repos){ + Log.debug("Fetching schematics from repos: @", repos); ui.showInfoFade("@schematic.browser.fetching", 2f); for (String link : repos){ Http.get(ghApi + "/repos/" + link + "/zipball/main", res -> handleRedirect(link, res), e -> Core.app.post(() -> { @@ -503,7 +557,7 @@ void setup(){ rebuild(); cont.pane( t -> { t.defaults().pad(5f); - t.pane ( p -> p.add(repoTable)).growX(); + t.pane(p -> p.add(repoTable)).growX(); }); } @@ -593,7 +647,7 @@ void close(){ } Core.settings.put("schematicrepositories", ui.schematicBrowser.repositoryLinks.toString(";")); Core.settings.put("hiddenschematicrepositories", ui.schematicBrowser.hiddenRepositories.toString(";")); - this.hide(); + hide(); } } } diff --git a/core/src/mindustry/core/UI.java b/core/src/mindustry/core/UI.java index ee08613701..efa3894ced 100644 --- a/core/src/mindustry/core/UI.java +++ b/core/src/mindustry/core/UI.java @@ -75,7 +75,6 @@ public class UI implements ApplicationListener, Loadable{ public PlanetDialog planet; public ResearchDialog research; public SchematicsDialog schematics; - public SchematicBrowserDialog schematicBrowser; public ModsDialog mods; public ColorPicker picker; public EffectsDialog effects; @@ -90,6 +89,7 @@ public class UI implements ApplicationListener, Loadable{ private @Nullable Element lastAnnouncement; // Client related + public SchematicBrowserDialog schematicBrowser; public UnitPicker unitPicker; public ClajManagerDialog clajManager; public ClajJoinDialog clajJoin; diff --git a/core/src/mindustry/game/Schematic.java b/core/src/mindustry/game/Schematic.java index 75ac43ec66..b90f59fbbb 100644 --- a/core/src/mindustry/game/Schematic.java +++ b/core/src/mindustry/game/Schematic.java @@ -39,8 +39,10 @@ public float powerConsumption(){ } public ItemSeq requirements(){ - ItemSeq requirements = new ItemSeq(); + return requirements(new ItemSeq()); + } + public ItemSeq requirements(ItemSeq requirements){ tiles.each(t -> { for(ItemStack stack : t.block.requirements){ requirements.add(stack.item, stack.amount); diff --git a/core/src/mindustry/ui/dialogs/SchematicsDialog.java b/core/src/mindustry/ui/dialogs/SchematicsDialog.java index 1261c476cb..173a36f2a2 100644 --- a/core/src/mindustry/ui/dialogs/SchematicsDialog.java +++ b/core/src/mindustry/ui/dialogs/SchematicsDialog.java @@ -14,7 +14,6 @@ import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; -import kotlin.Unit; import mindustry.client.*; import mindustry.client.communication.*; import mindustry.client.navigation.*; @@ -40,7 +39,6 @@ public class SchematicsDialog extends BaseDialog{ private Pattern ignoreSymbols = Pattern.compile("[`~!@#$%^&*()\\-_=+{}|;:'\",<.>/?]"); private Seq tags, selectedTags = new Seq<>(); private boolean checkedTags; - private static long previewTime; public SchematicsDialog(){ super("@schematics"); @@ -55,7 +53,6 @@ public SchematicsDialog(){ makeButtonOverlay(); shown(this::setup); onResize(this::setup); - update(() -> previewTime = 0); } void setup(){ @@ -308,14 +305,9 @@ public void showExport(Schematic s){ t.button("@schematic.chatshare", Icon.bookOpen, style, () -> { if (!state.isPlaying()) return; dialog.hide(); - clientThread.post(() -> { - Main.INSTANCE.send(new SchematicTransmission(s), () -> { - Core.app.post(() -> { - ui.showInfoToast(Core.bundle.get("client.finisheduploading"), 2f); - }); - return Unit.INSTANCE; - }); - }); + clientThread.post(() -> Main.INSTANCE.send(new SchematicTransmission(s), () -> Core.app.post(() -> + ui.showInfoToast(Core.bundle.get("client.finisheduploading"), 2f) + ))); }).marginLeft(12f).get().setDisabled(() -> !state.isPlaying()); }); }); @@ -730,6 +722,12 @@ public static class SchematicImage extends Image{ private Texture lastTexture; boolean set; + private static long previewTime; + + static { // Reset the preview time every frame + Events.run(EventType.Trigger.update, () -> previewTime = 0); + } + public SchematicImage(Schematic s){ super(Tex.clear); setScaling(Scaling.fit); @@ -778,14 +776,14 @@ public void draw(){ } private void setPreview(){ - if(Core.settings.getBool("restrictschematicloading", false) && previewTime > Time.millisToNanos(10) && !schematics.hasPreview(schematic)){ // Only allow 10ms of expensive preview creation each frame. Yes this is janky. No I don't care + if(Core.settings.getBool("restrictschematicloading", false) && previewTime > Time.millisToNanos(Core.settings.getInt("schemloadtime", 10)) && !schematics.hasPreview(schematic)){ // Only allow 10ms of expensive preview creation each frame. Yes this is janky. No I don't care set = false; return; } var start = Time.nanos(); TextureRegionDrawable draw = new TextureRegionDrawable(new TextureRegion(lastTexture = schematics.getPreview(schematic))); var time = Time.timeSinceNanos(start); - if(time > Time.millisToNanos(100)) Log.info("Schematic @ (@x@) took @ms to load", schematic.name(), schematic.width, schematic.height, time/(float)Time.nanosPerMilli); + if(time > Time.millisToNanos(50)) Log.debug("Schematic @ (@x@) took @ms to load", schematic.name(), schematic.width, schematic.height, time/(float)Time.nanosPerMilli); previewTime += time; setDrawable(draw); setScaling(Scaling.fit); diff --git a/gradle.properties b/gradle.properties index 5bad9a81b4..3617e93f3a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -28,4 +28,4 @@ org.gradle.internal.http.connectionTimeout=100000 #kapt.verbose=true # For some reason kapt ir is just completely broken for us. I don't know why. kapt.use.jvm.ir=false -archash=2863938b5e8a4756428faa0327d7616dcbc8511b +archash=6ba46537648804942be923d32e678d49602f010e