HUD slowdown with many patches drawn

Status
Not open for further replies.

Joat

Gum Phoenix
In my mod, the player has a grid-based inventory, with icons representing each item they have. When the amount of items in the inventory is fairly low, this works fine. However, when the inventory has a lot of items it (and thus a lot of icons to draw), the game slows down considerably.

Is there any way that anyone can think of to reduce or eliminate this problem (aside from using a text-based inventory or reducing the max size of the inventory)? I mean, due to the pseudo-pause when the inventory is open, this wouldn't get the player killed, but it is annoying.

I had the idea of having an array of cached patches (clearing a patch from the array when it is no longer in use), so that v.cachePatch would not need to be called for each item every single frame. While I did get that working, it was sadly not sufficient. It seems to be drawing the patch, not caching it, that is the issue here.

I don't have high hopes for there being a solution for this, but asking just in case.
 
Slowdown could also be caused by the way you coded the HUD function, perhaps? (I can't really tell since you have not shown us any Lua script code to check over.) Such as using "for player in players.iterate" where it isn't needed; I've known people previously not to understand that players.iterate does stuff for EVERY valid player present in the game rather than just the one you want.
 
Posting the code in question would make it a lot easier for us to provide specific assistance. There are so many different ways something could be slow or in need of optimizing that trying to provide general assistance is mostly just a shot in the dark.
 
Yes, the code was a rather silly thing for me to omit.

Code:
//This was used to attempt to speed patch drawing up by reducing the amount of v.cachePatch calls used. There is code at the end of the per-frame function to clear unused parts of the cache.
local function create_cachePatch(v,patchName)
	if cachedIcons[patchName] == nil then
		cachedIcons[patchName] = v.cachePatch(patchName)
		print("Added "..patchName.." to cache") //For debug purposes. Will naturally be removed before release. And yes, I have a corresponding message for removal, so I know the automatic cache clearing is working properly.
	end
	cacheUsed[patchName] = leveltime
	return cachedIcons[patchName]
end

//Return the string representing the icon for the item in question. This could be handled by an array IN THEORY, but some items change icon depending on circumstances.
local function getIcon(p,inventory,num)
	local item = inventory[num]
	if item == 1 then
		if P_IsObjectOnGround(p.mo) == false and p.powers[pw_shield] == 4 then
			return "ICONARMA"
		elseif interactText[p.mo.subsector.sector.tag] != nil then
			return "ICONINTE"
		elseif interactDialogue[p.mo.subsector.sector.tag] != nil then
			return "ICONINTT"
		else
			return "ICONINTN"
		end
	elseif item == 2 then
		if checkSectors(p,701) or checkSectors(p,702) or checkSectors(p,703) then return "ICONTHOP"
		else return "ICONTHER" end
	elseif item == 3 then return "ICONTHWA"
	elseif item == 4 then return "ICONTHHT"
	elseif item == 5 then return "ICONTHCD"
	elseif item == 6 then return "ICONREBR"
	//Empty Card
	elseif item == 7 then
		local nearestMonitor = nil //The nearest monitor within melee range.
		local monitorDist = 50*FRACUNIT //The distance away the nearest monitor is. The starting distance sets the max distance a monitor can be away to be affected by this item.
		for spot in mapthings.iterate do
			if spot.mobj != nil then
				local mon = spot.mobj
				if mon.type == MT_SUPERRINGBOX or mon.type == MT_BLACKTV or mon.type == MT_BLUETV or mon.type == MT_EGGMANBOX or mon.type == MT_GREENTV or mon.type == MT_PITYTV or mon.type == MT_INV or mon.type == MT_SNEAKERTV or mon.type == MT_WHITETV or mon.type == MT_YELLOWTV or mon.type == MT_FIRETV or mon.type == MT_WATERTV then //If it's ANY valid monitor type.
					if P_CheckSight(p.mo,mon) then
						if R_PointToDist2(p.mo.x,p.mo.y,mon.x,mon.y) < monitorDist then
							monitorDist = R_PointToDist2(p.mo.x,p.mo.y,mon.x,mon.y)
							nearestMonitor = mon
						end
					end
				end
			end
		end
		if nearestMonitor == nil then return "ICONCDEM" end
		if nearestMonitor.type == MT_SUPERRINGBOX or nearestMonitor.type == MT_SNEAKERTV then
			if p.energy >= 500 then return "ICONCDYS" else return "ICONCDNO" end
		elseif nearestMonitor.type == MT_PITYTV then
			if p.energy >= 1000 then return "ICONCDYS" else return "ICONCDNO" end
		elseif nearestMonitor.type == MT_FIRETV or nearestMonitor.type == MT_WATERTV then
			if p.energy >= 1500 then return "ICONCDYS" else return "ICONCDNO" end
		elseif nearestMonitor.type == MT_BLUETV or nearestMonitor.type == MT_BLACKTV or nearestMonitor.type == MT_GREENTV or nearestMonitor.type == MT_WHITETV or nearestMonitor.type == MT_YELLOWTV then
			if p.energy >= 2000 then return "ICONCDYS" else return "ICONCDNO" end
		elseif nearestMonitor.type == MT_EGGMANBOX or nearestMonitor.type == MT_INV then
			if p.energy >= 3000 then return "ICONCDYS" else return "ICONCDNO" end
		end
		return "ICONCDEM"
	elseif item == 8 then return "ICONCDRG"
	elseif item == 9 then return "ICONMEDI"
	elseif item == 10 then return "ICONCDAR"
	elseif item == 11 then return "ICONCDFO"
	elseif item == 12 then return "ICONCDEG"
	elseif item == 13 then return "ICONCDEL"
	elseif item == 14 then return "ICONCDPI"
	elseif item == 15 then return "ICONCDIN"
	elseif item == 16 then return "ICONCDSP"
	elseif item == 17 then return "ICONCDWH"
	elseif item == 18 then return "ICONCDMA"
	elseif item == 19 then return "ICONSLNG"
	elseif item == 20 then return "ICONCDFI"
	elseif item == 21 then return "ICONCDWA"
	elseif item == 22 then return "ICONFISD"
	else return "" end
end

//Return the number to show under the item's icon.
local function getItemNum(p,num,overrideShopMode)
	if p.shopMode == 1 and num > 0 then
		local item = shopInv
		if item == 19 then return shopSlingBullets
		elseif item == 22 then return shopSeeds
		else return "" end
	elseif p.shopMode == 3 and num > 0 then
		local item = buyBack
		if item == 19 then return shopSlingBulletsBuyback
		elseif item == 22 then return shopSeedsBuyback
		else return "" end
	else
		local item = p.slots[num]
		if item == 19 then return p.bullets
		elseif item == 22 then return p.seeds
		else return "" end
	end
end

//Return the string representing the name for the item in question. This could be handled by an array IN THEORY, but some items change name depending on circumstances.
local function getName(p,num)
	local item = p.slots[num]
	if item == 1 then
		if P_IsObjectOnGround(p.mo) == false and p.powers[pw_shield] == 4 then
			return "Detonate"
		elseif interactText[p.mo.subsector.sector.tag] != nil then
			return "Examine"
		elseif interactDialogue[p.mo.subsector.sector.tag] != nil then
			return "Talk"
		else
			return ""
		end
	elseif item == 2 then return "Thermos"
	elseif item == 3 then return "Water"
	elseif item == 4 then return "Warm Drink"
	elseif item == 5 then return "Cold Drink"
	elseif item == 6 then return "Rebreather"
	elseif item == 7 then return "Egg Card"
	elseif item == 8 then return "10 Rings"
	elseif item == 9 then return "Medikit"
	elseif item == 10 then return "Armageddon"
	elseif item == 11 then return "Force"
	elseif item == 12 then return "Robotnik"
	elseif item == 13 then return "Elemental"
	elseif item == 14 then return "Shield"
	elseif item == 15 then return "Invincibility"
	elseif item == 16 then return "Speed"
	elseif item == 17 then return "Whirlwind"
	elseif item == 18 then return "Attraction"
	elseif item == 19 then return "Sling"
	elseif item == 20 then return "Inferno"
	elseif item == 21 then return "Liquid"
	elseif item == 22 then return "Fire Seeds"
	else return "" end
end

local function drawInventory(v,p)
	if p.playerstate != PST_LIVE then return nil end //This should not show anything if the player is dead or if the menu is open.
	//Draw the top three boxes, the labels for them, and the items in them.
	v.drawString(100-v.stringWidth("SPIN")/2,10,"SPIN",V_SNAPTOTOP) //The button to press to use the item.
	if p.slots[-3] == 19 and p.attackTimer > 0 //Draw a semitransparent red box to indicate you can't attack again yet. As more weapons are coded in, the first condition will be replaced with isItemWeapon().
		v.draw(89,22,create_cachePatch(v,"NOATTACK"),V_SNAPTOTOP|V_50TRANS)
	end
	if p.slots[-3] > 0 then v.draw(91,24,create_cachePatch(v,getIcon(p,p.slots,-3)),V_SNAPTOTOP) end //Draw the icon for the item.
	v.drawString(108,34,getItemNum(p,-3),V_SNAPTOTOP,"right") //If the item has a quantity attached (for example, you can carry up to 99 Fire Seeds in one slot), draw it
	v.draw(88,21,create_cachePatch(v,"MENUCURS"),V_SNAPTOTOP) //Draw the box itself for the item.
	v.drawString(160-v.stringWidth("C1")/2,10,"C1",V_SNAPTOTOP) //Repeat two more times for the other boxes.
	if p.slots[-1] == 19 and p.attackTimer > 0
		v.draw(149,22,create_cachePatch(v,"NOATTACK"),V_SNAPTOTOP|V_50TRANS)
	end
	if p.slots[-1] > 0 then v.draw(151,24,create_cachePatch(v,getIcon(p,p.slots,-1)),V_SNAPTOTOP) end
	v.drawString(168,34,getItemNum(p,-1),V_SNAPTOTOP,"right")
	v.draw(148,21,create_cachePatch(v,"MENUCURS"),V_SNAPTOTOP)
	v.drawString(220-v.stringWidth("C2")/2,10,"C2",V_SNAPTOTOP)
	if p.slots[-2] == 19 and p.attackTimer > 0
		v.draw(209,22,create_cachePatch(v,"NOATTACK"),V_SNAPTOTOP|V_50TRANS)
	end
	if p.slots[-2] > 0 then v.draw(211,24,create_cachePatch(v,getIcon(p,p.slots,-2)),V_SNAPTOTOP) end
	v.drawString(228,34,getItemNum(p,-2),V_SNAPTOTOP,"right")
	v.draw(208,21,create_cachePatch(v,"MENUCURS"),V_SNAPTOTOP)
	//Draw the names if the screen is large enough and the menu is not open. Otherwise, don't bother. The text is either illegibly small or potentially covered up by the inventory.
	if not p.menuOpen and v.width() >= 640 and v.height() >= 400 then
		v.drawString(100-v.stringWidth(getName(p,-3),0,"small")/2,48,getName(p,-3),V_SNAPTOTOP,"small")
		v.drawString(160-v.stringWidth(getName(p,-1),0,"small")/2,48,getName(p,-1),V_SNAPTOTOP,"small")
		v.drawString(220-v.stringWidth(getName(p,-2),0,"small")/2,48,getName(p,-2),V_SNAPTOTOP,"small")
	end
	if not p.menuOpen then return nil end //Don't show the inventory menu if it's closed.
	//The global offset for all things on the menu. The menu should be centered on the screen horizontally. The global vertical offset will likely be consistently 50, but this may change.
	local offsetX = 7
	local offsetY = 50 //This should be AT LEAST 50.
	//Heading
	if p.shopMode == 0 then
		v.drawString(160,offsetY,"Inventory",0,"center") //Draw inventory heading.
	else
		v.drawString(160,offsetY,shopName,0,"center") //Draw inventory heading.
	end
	v.drawString(310,offsetY,"Rings: "..p.ringCount,0,"right") //Draw rings.
	v.draw(offsetX,offsetY+10,create_cachePatch(v,"INVENBOX")) //Draw main panel.
	v.draw(offsetX+218,offsetY+10,create_cachePatch(v,"SIDEBOX")) //Draw side panel.
	v.draw(offsetX,offsetY+124,create_cachePatch(v,"DESCBOX")) //Draw description panel.
	//Draw current inventory.
	local invUsed = p.slots //The inventory table to draw. This could be the player's, the shop's, or the shop's buyback (not yet implemented).
	if p.shopMode == 1 then invUsed = shopInv end
	//Draw inventory items.
	for i=1, 32
		if invUsed[i] > 0 then v.draw(offsetX+9+((i-1)%8)*26,offsetY+19+((i-1)/8)*26,create_cachePatch(v,getIcon(p,invUsed,i))) end //Draw item icon.
		v.drawString(offsetX+26+((i-1)%8)*26,offsetY+29+((i-1)/8)*26,getItemNum(p,i),0,"right") //Draw quantity, if applicable.
	end
	if p.shopMode > 0 then //If the player is in a shop.
		//Draw buy icon, translucent out if not in buy mode.
		if p.shopMode == 1 then
			v.draw(offsetX+227,offsetY+19,create_cachePatch(v,"ICONBUY"))
		else
			v.draw(offsetX+227,offsetY+19,create_cachePatch(v,"ICONBUY"),V_TRANSLUCENT)
		end
		//Draw sell icon, translucent out if not in sell mode.
		if p.shopMode == 2 then
			v.draw(offsetX+253,offsetY+19,create_cachePatch(v,"ICONSELL"))
		else
			v.draw(offsetX+253,offsetY+19,create_cachePatch(v,"ICONSELL"),V_TRANSLUCENT)
		end
		//Draw buyback icon, translucent out if not in buyback mode.
		if p.shopMode == 3 then
			v.draw(offsetX+279,offsetY+19,create_cachePatch(v,"ICONBUYB"))
		else
			v.draw(offsetX+279,offsetY+19,create_cachePatch(v,"ICONBUYB"),V_TRANSLUCENT)
		end
		//Draw sell price.
		if p.shopMode == 2 and p.slots[8*p.cY+p.cX+1] > 0 then
			if price[p.slots[8*p.cY+p.cX+1]] == 0 then //If the selected item has no price...
				v.drawString(10,offsetY,"NO SALE") //...it cannot be sold.
			else
				v.drawString(10,offsetY,"Price: "..(price[p.slots[8*p.cY+p.cX+1]]/2)) //Otherwise, draw its selling price.
			end
		end
		//Draw shop option descriptions in the description panel.
		if p.cX == 8 and p.cY == 0 then
			if v.width() < 640 or v.height() < 400 then
				v.drawString(offsetX+5,offsetY+129,"Buy items for rings",0,"thin")
			else
				v.drawString(offsetX+5,offsetY+129,"Buy items for rings.",0,"small")
			end
		elseif p.cX == 9 and p.cY == 0 then
			if v.width() < 640 or v.height() < 400 then
				v.drawString(offsetX+5,offsetY+129,"Sell items for rings",0,"thin")
			else
				v.drawString(offsetX+5,offsetY+129,"Sell items for rings.",0,"small")
			end
		elseif p.cX == 10 and p.cY == 0 then
			if v.width() < 640 or v.height() < 400 then
				v.drawString(offsetX+5,offsetY+129,"Buy items back at selling price",0,"thin")
			else
				v.drawString(offsetX+5,offsetY+129,"Buy items back at selling price.",0,"small")
				v.drawString(offsetX+5,offsetY+135,"Items not bought back are cleared at the end of each level.",0,"small")
			end
		end
	else //If the player is viewing their inventory normally, outside of a shop.
		//Draw converter.
		if p.convertMode == 1 then v.draw(offsetX+227,offsetY+97,create_cachePatch(v,"ICONCONH")) end
		if p.convertMode == 2 then v.draw(offsetX+227,offsetY+97,create_cachePatch(v,"ICONCONE")) end
		//Draw batteries owned.
		if p.maxEnergy >= 1 then
			if leveltime%2 == 1 then
				v.draw(offsetX+248,offsetY+93,create_cachePatch(v,"ICONBATT"))
			else
				v.draw(offsetX+248,offsetY+93,create_cachePatch(v,"ICONBAT2"))
			end
			if p.maxEnergy < 10 then v.drawString(offsetX+263,offsetY+104,p.maxEnergy)
			else v.drawString(offsetX+262,offsetY+104,p.maxEnergy,0,"thin") end
		end
		//Draw any emeralds currently owned.
		if emeralds & EMERALD1 then v.draw(offsetX+284,offsetY+102,create_cachePatch(v,"TEMER1")) end
		if emeralds & EMERALD2 then v.draw(offsetX+280,offsetY+96,create_cachePatch(v,"TEMER2")) end
		if emeralds & EMERALD3 then v.draw(offsetX+288,offsetY+96,create_cachePatch(v,"TEMER3")) end
		if emeralds & EMERALD4 then v.draw(offsetX+292,offsetY+102,create_cachePatch(v,"TEMER4")) end
		if emeralds & EMERALD5 then v.draw(offsetX+288,offsetY+108,create_cachePatch(v,"TEMER5")) end
		if emeralds & EMERALD6 then v.draw(offsetX+280,offsetY+108,create_cachePatch(v,"TEMER6")) end
		if emeralds & EMERALD7 then v.draw(offsetX+276,offsetY+102,create_cachePatch(v,"TEMER7")) end
		//Battery description.
		if p.cX == 9 and p.cY == 3 and p.maxEnergy > 0 then
			if v.width() < 640 or v.height() < 400 then
				v.drawString(offsetX+5,offsetY+129,"Your batteries. Used to power devices built by Eggman.",0,"thin")
			else
				v.drawString(offsetX+5,offsetY+129,"Your collected Egg Batteries.",0,"small")
				v.drawString(offsetX+5,offsetY+135,"Used to power any of Eggman's devices you have taken.",0,"small")
			end
		//Emeralds description.
		elseif p.cX == 10 and p.cY == 3 and emeralds != 0 then
			if v.width() < 640 or v.height() < 400 then
				v.drawString(offsetX+5,offsetY+129,"Your chaos emeralds. Can often boost devices built by Eggman.",0,"thin")
			else
				v.drawString(offsetX+5,offsetY+129,"Your collected chaos emeralds.",0,"small")
				v.drawString(offsetX+5,offsetY+135,"Devices built by Eggman can often be boosted by these.",0,"small")
			end
		//Converter description.
		elseif p.cX == 8 and p.cY == 3 and p.convertMode != 0 then
			if v.width() < 640 or v.height() < 400 then
				v.drawString(offsetX+5,offsetY+129,"Converts rings to health or energy     Select to toggle preference",0,"thin")
			else
				v.drawString(offsetX+5,offsetY+129,"This converter converts collected rings to health or energy.",0,"small")
				v.drawString(offsetX+5,offsetY+135,"Select to toggle which one is prioritized.",0,"small")
			end
		//Debug room description.
		elseif p.cY == 1 and p.cX == 9 and gamemap != 172 then
			if v.width() < 640 or v.height() < 400 then
				v.drawString(offsetX+5,offsetY+129,"Nothing to see in this slot",0,"thin")
			else
				v.drawString(offsetX+5,offsetY+129,"Nothing to see in this slot.",0,"small")
				v.drawString(offsetX+5,offsetY+135,"Yup, completely blank.",0,"small")
			end
		end
	end
	//Draw item selection cursor.
	if (leveltime/10)%2 == 1 then
		if p.cX < 8 then v.draw(offsetX+6+(p.cX*26),offsetY+16+(p.cY*26),create_cachePatch(v,"MENUCURS"))
		else v.draw(offsetX+16+(p.cX*26),offsetY+16+(p.cY*26),create_cachePatch(v,"MENUCURS")) end
	end
	//Draw the selected item's name and description.
	if p.cX < 8 and invUsed[8*p.cY+p.cX+1] > 0 then
		//Draw a single thin line if at low res.
		if v.width() < 640 or v.height() < 400 then
			v.drawString(offsetX+5,offsetY+129,itemName[invUsed[8*p.cY+p.cX+1]]..":",V_YELLOWMAP,"thin")
			v.drawString(offsetX+5+v.stringWidth(itemName[invUsed[8*p.cY+p.cX+1]]..":  ",0,"thin"),offsetY+129,itemShortDesc[invUsed[8*p.cY+p.cX+1]],0,"thin")
		else
			v.drawString(offsetX+5,offsetY+129,itemName[invUsed[8*p.cY+p.cX+1]]..":",V_YELLOWMAP,"small")
			v.drawString(offsetX+5+v.stringWidth(itemName[invUsed[8*p.cY+p.cX+1]]..":  ",0,"small"),offsetY+129,itemDesc[invUsed[8*p.cY+p.cX+1]],0,"small")
			v.drawString(offsetX+5,offsetY+135,itemDesc2[invUsed[8*p.cY+p.cX+1]],0,"small")
		end
	end
end

hud.add(drawInventory,"game")

Yes, quite a lot of inventory-related code, though I omitted the lump-wide variable declarations themselves in this posting. I also added some of the comments in significantly after the coding process for their respective parts, so my apologies if they contain any errors.
 
Alright, there are a few things I can see off the top of my head that might affect it:

  • The patch-caching code seems like a good idea on paper, but ultimately, the Lua interpreter is so much slower compared to native code that you lose all of the benefits and then some. Plus, I'm somewhat sure v.cachePatch() already holds patches cached anyway.
  • In getIcon for item 7, the mapthings iteration is fairly hefty code-wise. I'd try to avoid calling that as much as possible.
It seems that, in general, deciding the patch to render for each item might be a slower bit of the code. You can try computing the patch name for each item only when necessary (likely once when you open the inventory and once each time the contents change while the inventory is open, since you said the game pauses while it's open) and store those somewhere. That way, your item rendering loop just needs to pass the name into v.cachePatch(). (You could even store the cached patches themselves when you refresh the patch list, since those persist between frames; however, I haven't checked that bit of the code recently and you might have to deal with memory management cleaning those up on you.)
 
My thanks to thee! That was, indeed, the issue. I have corrected the code so that the icon checking code for item 7 is done only once per frame. Though, I'll shortly revise it so that it doesn't re-check while the player is pseudo-paused.

As for your proposal on avoiding running getIcon so oft, that does make sense, indeed. Though, I do wonder how the benefit of that solution may compare to the cost of having an extra array of patch names in memory.
 
Holding things in memory generally isn't a problem performance-wise. The only time I've had problems from that was storing 4 minutes+ (around 6000 entries) of object position and momentum data that was constantly being appended to, and the appending was more likely the culprit than the memory itself. (This was also back when garbage collection ran way too often, and on my old crusty laptop to boot)
 
Status
Not open for further replies.

Who is viewing this thread (Total: 1, Members: 0, Guests: 1)

Back
Top