From c7e7f7e58cda7a499513d713ea8b338fc4262d73 Mon Sep 17 00:00:00 2001 From: Anuken Date: Mon, 15 Jun 2026 15:07:18 -0400 Subject: [PATCH] More block rendering optimization --- .../plastanium-conveyor-stack.png | Bin 1435 -> 1010 bytes .../stack-conveyors/surge-conveyor-stack.png | Bin 244 -> 1141 bytes .../mindustry/entities/comp/BuildingComp.java | 2 +- .../src/mindustry/graphics/BlockRenderer.java | 209 +++++++++++++----- .../graphics/BuildingCacheLayer.java | 22 ++ core/src/mindustry/world/Block.java | 2 + .../blocks/distribution/StackConveyor.java | 12 +- 7 files changed, 186 insertions(+), 61 deletions(-) create mode 100644 core/src/mindustry/graphics/BuildingCacheLayer.java diff --git a/core/assets-raw/sprites/blocks/distribution/stack-conveyors/plastanium-conveyor-stack.png b/core/assets-raw/sprites/blocks/distribution/stack-conveyors/plastanium-conveyor-stack.png index 8cd2516e5cf12916fc6232e81b9c51cfae2bc452..aa10d1419b3f58c64a3df2bb932de5f6537534a5 100644 GIT binary patch delta 989 zcmV<310wvJ3-Sk$BYyw^b5ch_0olnce*gdg1ZP1_K>z@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4& zKXw2B00(qQO+^Rl1`iVi2-yE8tpET6NJ&INR9M69mrZXSHGdR_A0L0rBubh#T_OGh zLLdaC8-5Jg(GL~~A=Zdh2<#&GJ$x+yiT_X`)P}YRC^NQ?&0;(^$<+-_`AQbGuH~80 zvCr%G_`Js#UbIVY=5N@a?sKbcET^9TRR9LCfV6S$^L%a2a~rh*ss9v!Ips6}9pGCJ zr{CkQzGY#7DZJTdlM^&Se$dMd%Sntzq@lC7#cI>c>vf(_r~tt{;x4c zVvM91i7BG06jf?NAY!p1cpvb=6GBG_0p~pL-`m*&j(-~y^&$YI(Y~>}xBn={$T*G+ z!|?cgFfa_q z48uT5k)J(&RXJ#W8rM86j3d(W*`KknO^sLQ)u8GF@C&Y2iT z;y4oH$bZI-YmDuA4oi%YIF5|t$T$paY+pNh9}_8NaxQ%T^QXfKf4CL&TcA@_QkvqP zQety^tA!w;Ia6zy3BZWKma<@E^X7H_cyh=%CcJlqtWMc|U+2RQ-g~)i*qq0}ta<|2S_`E)s467F_^iFAVwDNU@)c;XTUIhKQqS|j1lyg+niSy zmVX(jO|haCnetogBF{wx5m|CDITevfl4sjCzJBX~H*P=Vo1Z>;u&P|U^8koF(RS}V z&bjFT&W6irC>Cd%y$?9&cL+Do4LPWpjJ0X}>}B zB=4oM<-I3_z|p~^hFzEN-eR3?VY?Uu3RVo(I-FBN=%)0mmERv9HYv8xl;W=I=(>mx z4(Eh7U-_tCRS~abF}n0}d?hXaB^>EX>4U6ba`-PAZ2)IW&i+q+U-|cb{s1V z{AU$e0s@4@a(JF|cW{@V65aE%$74J3XBtmK3lLJ31lZ00|6A@Co{0iuXH9-n!h<16HRn;_|ww|_^ulYZ*aQ_KrtN10nW88zb zB*wDe@*F#NZN35T+IoYXuctgdLg9Qq2jn*xopR#m*lmRFEu!1X4;=ts{QYx}-5R>q zJiMzZ1F7szr++T4>=p(B&J_2wj3W54H`P`Oio(SX#s=7IDBV$^0JIk+8dPb~+;yi8 z6H}N$P>0Y%+%k<4O6Y)}KnDt~AfREms9>dowFq&z+CqEXve)aRSa}deA{jc?4eyJ4 zCj66fRkW6cF0xOrSi!DfF>N^s=Io78AcWS5DQ|ptaesGl> zC5wnkk$3mRT2KZN-%que@aGDyv>uJ6U~XjovW# znl(CEGse;$CpXq07P+V(+fLXx17jf(7#Cvz0e>BgGq03VFy@RiUjmYX16U(Aat0Y= zU@%Pzyy(i^jk%Y+IZof=EquhBGwObVIcLGal@JM#rdn0taTya+RR)v*Uk=mVZxvOjWuSB`>r(~ zS%2s5&HJ`>=G|@%yC2mp_PZXl+Fk4reQajOuF#}5pRq4QUuUnb>9D%>(YwsPdatV6 zG9(=(=~-~i9Hn4uDMOka9SeCpOKv#q%>%aUpgX+r{)3tpGJC77mua~1tfTiPq2w-& z=XLf&GGo^m#C@ptxUi>Kpz-~qd0iGLa{ z%x~h|4=}%pcaO|_%Qnj-T#FUWPjW6;w$*(QyPa{LBpmHLGbb(8RB&)k9GT@V)jk8; zvjqgNh`T`=vE8AcAEUv&)bdX2!94ph`VqtSf7kAI-_rBKI`1Mq(9CpSk!yDF$bz$1+mQ0i;?^iX#|lWjV2qV5Ftx#5jVHRLdn5 zW${|}{QhG&+uZIsoNc68hSe@6QhY+la(rseUGMr&kpqaaoE)`Gw9Mlt4@nYK!$RwV z2IdG{36Y6ffewIG_Ta!o!GNa$82J6ze&8HnZugwQ-0nH$*^pezsTKvuB^))?k`w~O x7>r8|uA+GGRz`!OIvx}a5?VLOM!_f;1pxfVM%pC3Kmq^&002ovPDHLkV1mZ>l1cyo diff --git a/core/assets-raw/sprites/blocks/distribution/stack-conveyors/surge-conveyor-stack.png b/core/assets-raw/sprites/blocks/distribution/stack-conveyors/surge-conveyor-stack.png index d953c790a4439178d4308ee56d13f4342774c7d1..32badcd095a09ddc0179a2757b1ce40662110f57 100644 GIT binary patch delta 1115 zcmV-h1f=`)0rd!wDSrXk$t-^W000DMK}|sb0I`n?{9y$E001CkNK#Dz0D2|>0Dy!5 z0Qvv`0D$NK0Cg|`0P0`>06Lfe02gqax=}m;000SaNLh0L01FcU01FcV0GgZ_00007 zbV*G`2kHh769gQ>CG*e#00YZOL_t(o!|j(#ZyZGs$A8_`Gk^PvSs#eZQwkCi%7up& z4v_Bvh@c1#oQXi35eP0|Bp)n`kb?vv|pnn0hfF_V9F-gw>Rgwbe0c9d}phwgp;3ffe-Z}Td4Iez-2Vw|>;E6Hv zX7>-2QW&iftudm>M0k0Ac?P(ZNV)w;s6)V8Kzm_v=@)A)t1D|b=kVU)Lm_G?}Dz;+6Wza0Yom`^P%F8$gEFNz*zSz?`KWBsQ=2>JxH7G9a& zCO1Mm&*(JrQ4+_2nz&K*8e;s2Yp=+|vB}v3Wt)~w2 z!2i1LI|8v^*R|H{5)rg$l+q}rP-^JO{%@r;T9U5T7?VVjjryK=P!0?_n^wCChzQ=- z0Dn*?-6)Bgd+(%$pcm;L&ju0@#G_vWkHN}5%VaN8n1+6ge=R* zvW&B{o3t8D8kwm*pcGmw_EthOAp`-aM1)`r&ph=wy`3F`A67oCG(PBwl`u0oXP)1r z*~n=&4UJsLj3A;h&wZNRZU{=PH78F#O@CEYLm;KlqH)^d{caD42qw#TV; zg7eIf8BJq$t~qHyIj|d^zp#AETFYoRtZc>l^&iK&q1Kw$r*DpR!-g@On!DJ!lWw?c z9{A$RJ)+nee`tivRQP hf_wgD^kCid^$(FlR$(dZUOWH*002ovPDHLkV1i0s1Bn0t delta 211 zcmV;^04)FY2=oDvDSrU2&K&6g006E@L_t(og=1hC1*2f314d#M<`p#j$0nX8_!_Hv zo%*RnnN5_U=`$DqXJBApxPJXER(VPsK~MqAau}etBk)S1TTTl{;FQQKX!x(Gsf^Ek zSV4x>9qbnAHe$7l37=uOe2NduIHrPC{Qu>8tf7h50rYSLK}-iw+Yw|rfR caches = new Seq<>(); - private Seq queuedCacheDraws = new Seq<>(); - private IntSeq queuedCacheIndices = new IntSeq(); + + private Seq[] caches = new Seq[BuildingCacheLayer.amount]; + private Seq[] queuedCacheDraws = new Seq[BuildingCacheLayer.amount]; + private IntSeq[] queuedCacheIndices = new IntSeq[BuildingCacheLayer.amount]; private IntSet dirtyChunks = new IntSet(); + //only used when blockDrawCountDebug = true + private ObjectIntMap blockDrawSprites = new ObjectIntMap<>(); + private ObjectIntMap blockDrawTotal = new ObjectIntMap<>(); + public BlockRenderer(){ + for(int i = 0; i < BuildingCacheLayer.amount; i++){ + caches[i] = new Seq<>(); + queuedCacheDraws[i] = new Seq<>(); + queuedCacheIndices[i] = new IntSeq(); + } Events.on(ClientLoadEvent.class, e -> { cracks = new TextureRegion[maxCrackSize][crackRegions]; @@ -94,7 +107,7 @@ public class BlockRenderer{ if(blockTree == null || floorTree == null || overlayTree == null) return; if(event.tile.block().drawCached){ - recacheBuilding(event.tile); + recacheBuilding(event.tile.block().buildingCacheLayer, event.tile); } if(indexBlock(event.tile)){ @@ -115,7 +128,7 @@ public class BlockRenderer{ } if(event.tile.block().drawCached){ - recacheBuilding(event.tile); + recacheBuilding(event.tile.block().buildingCacheLayer, event.tile); } if(visible){ @@ -136,7 +149,7 @@ public class BlockRenderer{ }); } - public void recacheBuilding(Tile tile){ + public void recacheBuilding(BuildingCacheLayer layer, Tile tile){ if(cacheChunks == null) return; int cx = tile.x / chunkSize, cy = tile.y / chunkSize; @@ -147,7 +160,7 @@ public class BlockRenderer{ if(chunk == null) return; - chunk.dirty = true; + chunk.dirty[layer.ordinal()] = true; int packed = Point2.pack(cx, cy); //don't re-cache the chunk unless it was in view if(chunksToDrawSet.contains(packed)){ @@ -155,25 +168,31 @@ public class BlockRenderer{ } } - public void cacheChunk(int cx, int cy){ + public void cacheChunk(int layer, int cx, int cy){ int required = chunkSize * chunkSize * maxSpritesPerCacheTile; CacheChunk chunk = cacheChunks[cx][cy]; if(chunk == null){ chunk = cacheChunks[cx][cy] = new CacheChunk(); - if(caches.isEmpty() || caches.peek().getSpritesUsed() + required > caches.peek().getSpriteCapacity()){ - //there's no sense adding leftover space, since it will never be used - caches.add(new SpriteCache(16382 - (16382 % required), true)); - queuedCacheDraws.add(new IntSeq()); - } - chunk.cache = caches.peek(); - chunk.spriteCacheIndex = caches.size - 1; - chunk.cache.beginCache(); - }else{ - chunk.cache.beginCache(chunk.id); } - cbatch.cache = chunk.cache; + SpriteCache cache = chunk.caches[layer]; + if(cache == null){ + var cacheArr = caches[layer]; + if(cacheArr.isEmpty() || cacheArr.peek().getSpritesUsed() + required > cacheArr.peek().getSpriteCapacity()){ + //there's no sense adding leftover space, since it will never be used + cacheArr.add(new SpriteCache(16382 - (16382 % required), true)); + queuedCacheDraws[layer].add(new IntSeq()); + } + cache = chunk.caches[layer] = cacheArr.peek(); + chunk.spriteCacheIndices[layer] = cacheArr.size - 1; + cache.beginCache(); + }else{ + cache = chunk.caches[layer]; + cache.beginCache(chunk.cacheIds[layer]); + } + + cbatch.cache = cache; Batch lastBatch = Core.batch; try{ Draw.flush(); @@ -184,7 +203,7 @@ public class BlockRenderer{ blockCachedTree.intersect(cx * chunkSize * tilesize, cy * chunkSize * tilesize, chunkSize * tilesize, chunkSize * tilesize, tile -> { //only draw blocks strictly inside the chunk - if(!(tile.x >= x1 && tile.x < x2 && tile.y >= y1 && tile.y < y2) || !tile.block().drawCached) return; + if(!(tile.x >= x1 && tile.x < x2 && tile.y >= y1 && tile.y < y2) || !tile.block().drawCached || tile.block().buildingCacheLayer.ordinal() != layer) return; Block block = tile.block(); Building build = tile.build; @@ -208,9 +227,9 @@ public class BlockRenderer{ batch = lastBatch; } - chunk.dirty = false; - chunk.cache.reserve(required); - chunk.id = chunk.cache.endCache(); + chunk.dirty[layer] = false; + cache.reserve(required); + chunk.cacheIds[layer] = cache.endCache(); } public void reload(){ @@ -220,10 +239,12 @@ public class BlockRenderer{ overlayTree = new OverlayQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight())); floorTree = new FloorQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight())); - for(SpriteCache cache : caches){ - cache.dispose(); + for(var arr : caches){ + for(SpriteCache cache : arr){ + cache.dispose(); + } + arr.clear(); } - caches.clear(); int chunksx = Mathf.ceil((float)(world.width()) / chunkSize), chunksy = Mathf.ceil((float)(world.height()) / chunkSize); cacheChunks = new CacheChunk[chunksx][chunksy]; @@ -577,29 +598,43 @@ public class BlockRenderer{ } }); - queuedCacheIndices.clear(); - - for(var seq : queuedCacheDraws){ + for(var seq : queuedCacheIndices){ seq.clear(); } + for(var arr : queuedCacheDraws){ + for(var seq : arr){ + seq.clear(); + } + } + //begin by checking any stale/uncached chunks, and caching them if necessary chunksToDraw.each(xy -> { int cx = Point2.x(xy); int cy = Point2.y(xy); CacheChunk chunk = cacheChunks[cx][cy]; - if(chunk == null || chunk.dirty || chunk.lastSeenTeam != pteam){ - cacheChunk(cx, cy); + if(chunk == null || chunk.lastSeenTeam != pteam){ + for(int i = 0; i < BuildingCacheLayer.amount; i++){ + cacheChunk(i, cx, cy); + } + }else{ + for(int i = 0; i < BuildingCacheLayer.amount; i++){ + if(chunk.dirty[i]){ + cacheChunk(i, cx, cy); + } + } } //in case it was null (it can never be null after caching) chunk = cacheChunks[cx][cy]; - //record the sprite cache IDs that will be drawn in queuedCacheIndices, and record the actual cache IDs of the respective sprite caches in queuedCacheDraws - IntSeq cacheIDsToDraw = queuedCacheDraws.get(chunk.spriteCacheIndex); - if(cacheIDsToDraw.isEmpty()){ - queuedCacheIndices.add(chunk.spriteCacheIndex); + for(int layer = 0; layer < BuildingCacheLayer.amount; layer++){ + //record the sprite cache IDs that will be drawn in queuedCacheIndices, and record the actual cache IDs of the respective sprite caches in queuedCacheDraws + IntSeq cacheIDsToDraw = queuedCacheDraws[layer].get(chunk.spriteCacheIndices[layer]); + if(cacheIDsToDraw.isEmpty()){ + queuedCacheIndices[layer].add(chunk.spriteCacheIndices[layer]); + } + cacheIDsToDraw.add(chunk.cacheIds[layer]); } - cacheIDsToDraw.add(chunk.id); }); lastCamX = avgx; @@ -636,22 +671,43 @@ public class BlockRenderer{ if(chunksToDraw.size > 0){ - Draw.draw(Layer.block, () -> { - dirtyChunks.each(c -> cacheChunk(Point2.x(c), Point2.y(c))); - dirtyChunks.clear(); + for(int mlayer = 0; mlayer < BuildingCacheLayer.amount; mlayer++){ + int layer = mlayer; + float z = BuildingCacheLayer.layers[layer]; - //minor optimization: don't transfer the matrix every time - SpriteCache.getDefaultShader().bind(); - SpriteCache.getDefaultShader().setUniformMatrix4("u_projectionViewMatrix", camera.mat); + Draw.draw(z, () -> { + if(layer == 0){ + dirtyChunks.each(c -> { + int cx = Point2.x(c), cy = Point2.y(c); + var chunk = cacheChunks[cx][cy]; - queuedCacheIndices.each(spriteCacheIndex -> { - SpriteCache sprites = caches.get(spriteCacheIndex); - IntSeq cachesToDraw = queuedCacheDraws.get(spriteCacheIndex); - sprites.begin(false); - cachesToDraw.each(sprites::draw); - sprites.end(); + for(int l = 0; l < BuildingCacheLayer.amount; l++){ + if(chunk == null || chunk.dirty[l]){ + cacheChunk(l, Point2.x(c), Point2.y(c)); + } + } + }); + dirtyChunks.clear(); + + //minor optimization: don't transfer the matrix every time + SpriteCache.getDefaultShader().bind(); + SpriteCache.getDefaultShader().setUniformMatrix4("u_projectionViewMatrix", camera.mat); + } + + queuedCacheIndices[layer].each(spriteCacheIndex -> { + SpriteCache sprites = caches[layer].get(spriteCacheIndex); + IntSeq cachesToDraw = queuedCacheDraws[layer].get(spriteCacheIndex); + sprites.begin(false); + cachesToDraw.each(sprites::draw); + sprites.end(); + }); }); - }); + } + } + + if(blockDrawCountDebug){ + blockDrawTotal.clear(); + blockDrawSprites.clear(); } //draw most tile stuff @@ -666,6 +722,7 @@ public class BlockRenderer{ boolean visible = (build == null || !build.inFogTo(pteam)); if(block != Blocks.air && (visible || build.wasVisible)){ + SpriteBatch.totalDrawCalls = 0; block.drawBase(tile); Draw.reset(); Draw.z(Layer.block); @@ -700,9 +757,45 @@ public class BlockRenderer{ } } Draw.reset(); + + if(blockDrawCountDebug){ + blockDrawTotal.increment(block, 1); + if(SpriteBatch.totalDrawCalls > 0) blockDrawSprites.increment(block, (int)SpriteBatch.totalDrawCalls); + } } } + if(blockDrawCountDebug && graphics.getFrameId() % 30 == 0){ + class Entry implements Comparable{ + Block block; + int amount; + + public Entry(Block block, int amount){ + this.block = block; + this.amount = amount; + } + + @Override + public String toString(){ + return block + ": " + amount + " (average: " + Strings.autoFixed((float)amount/blockDrawTotal.get(block), 1) + ")"; + } + + @Override + public int compareTo(Entry entry){ + return Integer.compare(amount, entry.amount); + } + } + int total = 0; + + Seq entries = new Seq<>(); + for(var v : blockDrawSprites){ + entries.add(new Entry(v.key, v.value)); + total += v.value; + } + entries.sort().reverse(); + Log.info("Draw calls:\n" + entries.toString("\n") + "\nTOTAL: " + total + "\n"); + } + //draw overlay of extra cached tiles (they otherwise wouldn't draw cracks / status overlays) for(int i = 0; i < tileExtraCachedView.size; i++){ Building build = tileExtraCachedView.items[i]; @@ -716,7 +809,7 @@ public class BlockRenderer{ build.wasVisible = true; updateShadow(build); renderer.minimap.update(build.tile); - recacheBuilding(build.tile); + recacheBuilding(build.block.buildingCacheLayer, build.tile); } } @@ -791,11 +884,17 @@ public class BlockRenderer{ } static class CacheChunk{ - SpriteCache cache; - int spriteCacheIndex; //index of cache variable in list of global spritcaches - int id; - boolean dirty = true; + SpriteCache[] caches = new SpriteCache[BuildingCacheLayer.amount]; + //index of cache variable in list of global sprite caches + int[] spriteCacheIndices = new int[BuildingCacheLayer.amount]; + int[] cacheIds = new int[BuildingCacheLayer.amount]; + boolean[] dirty = new boolean[BuildingCacheLayer.amount]; + Team lastSeenTeam; + + { + Arrays.fill(dirty, true); + } } static class BlockQuadtree extends QuadTree{ diff --git a/core/src/mindustry/graphics/BuildingCacheLayer.java b/core/src/mindustry/graphics/BuildingCacheLayer.java new file mode 100644 index 0000000000..11bc0cb931 --- /dev/null +++ b/core/src/mindustry/graphics/BuildingCacheLayer.java @@ -0,0 +1,22 @@ +package mindustry.graphics; + +public enum BuildingCacheLayer{ + under(Layer.block - 0.2f), + normal(Layer.block); + + public final float layer; + + public final static BuildingCacheLayer[] all = values(); + public final static int amount = all.length; + public final static float[] layers = new float[all.length]; + + static{ + for(int i = 0; i < amount; i++){ + layers[i] = all[i].layer; + } + } + + BuildingCacheLayer(float layer){ + this.layer = layer; + } +} diff --git a/core/src/mindustry/world/Block.java b/core/src/mindustry/world/Block.java index c5cf127a41..50ee93dcc3 100644 --- a/core/src/mindustry/world/Block.java +++ b/core/src/mindustry/world/Block.java @@ -235,6 +235,8 @@ public class Block extends UnlockableContent implements Senseable{ public boolean drawDynamic = true; /** If enabled, drawCached() will be called on the building. */ public boolean drawCached = false; + /** */ + public BuildingCacheLayer buildingCacheLayer = BuildingCacheLayer.normal; /** Special flag; if false, floor will be drawn under this block even if it is cached. */ public boolean fillsTile = true; /** If true, this block can be covered by darkness / fog even if synthetic. */ diff --git a/core/src/mindustry/world/blocks/distribution/StackConveyor.java b/core/src/mindustry/world/blocks/distribution/StackConveyor.java index 86da096357..d12b2bca04 100644 --- a/core/src/mindustry/world/blocks/distribution/StackConveyor.java +++ b/core/src/mindustry/world/blocks/distribution/StackConveyor.java @@ -53,6 +53,8 @@ public class StackConveyor extends Block implements Autotiler{ conveyorPlacement = true; underBullets = true; priority = TargetPriority.transport; + drawCached = true; + buildingCacheLayer = BuildingCacheLayer.under; ambientSound = Sounds.loopConveyor; ambientSoundVolume = 0.004f; @@ -119,9 +121,7 @@ public class StackConveyor extends Block implements Autotiler{ boolean proxUpdating = false; @Override - public void draw(){ - Draw.z(Layer.block - 0.2f); - + public void drawCached(){ Draw.rect(regions[state], x, y, rotdeg()); for(int i = 0; i < 4; i++){ @@ -145,12 +145,14 @@ public class StackConveyor extends Block implements Autotiler{ Draw.rect(sliced(regions[0], SliceMode.top), x + Geometry.d4x(rotation) * tilesize*0.75f, y + Geometry.d4y(rotation) * tilesize*0.75f, rotation * 90f); } } + } + @Override + public void draw(){ Draw.z(Layer.block - 0.1f); Tile from = world.tile(link); - //TODO do not draw for certain configurations? if(glowRegion.found() && power != null && power.status > 0f){ Draw.z(Layer.blockAdditive); Draw.color(glowColor, glowAlpha * power.status); @@ -185,7 +187,6 @@ public class StackConveyor extends Block implements Autotiler{ //item float size = itemSize * Mathf.lerp(Math.min((float)items.total() / itemCapacity, 1), 1f, 0.4f); - Drawf.shadow(Tmp.v1.x, Tmp.v1.y, size * 1.2f); Draw.rect(lastItem.fullIcon, Tmp.v1.x, Tmp.v1.y, size, size, 0); } @@ -215,6 +216,7 @@ public class StackConveyor extends Block implements Autotiler{ @Override public void onProximityUpdate(){ super.onProximityUpdate(); + recache(); int lastState = state;