How to properly cache sprites/labels/textures and reduce draw calls?

HeikofantHeikofant USUniversity ✭✭

Hello,
I am currently having the following issue: I created an interface myself with several buttons (each button is derived from CCNode).
Each button consists of different images (CCSprite) and Texts (CCLabel).
Unfortunately, each sprite and each label causes an increase in the draw calls.

I am loading and caching the images the following way:

.....
var buttonsFile = "buttons.plist";
var buttons = new CCSpriteSheet(buttonsFile);
CCSpriteFrameCache.SharedSpriteFrameCache.AddSpriteFrames(buttonsFile);
CCSpriteSheetCache.Instance.AddSpriteSheet(buttonsFile);
foreach(var button in buttons.Frames){
  CCTextureCache.SharedTextureCache.AddImage(button.TextureFileName);
}

And then later when creating a button

...
var background = new CCSprite(buttons.Frames.Find(frame => frame.TextureFilename.StartsWith("button_background")));
this.AddChild(background);

.... adding further sprites ...

var label = new CCLabel("Button","arial",30,CCLabelFormat.SpriteFont);
this.AddChild(label);

Unforutnately, this increases the draw calls for each label and sprite added to that button.

I discovered that when I was inheriting my buttons from CCMenuItemImage and setting NormalImage to the sprite and additionally adding the sprite with this.AddChild(button), the draw calls where not increased and I needed only one draw call for that sprite in all buttons.

Can anybody tell me how I should cache and create my sprites in order to keep the draw calls low? I thought about CCSpriteBatchNode, but that's marked as obsolete/deprecated.

Best Answer

Answers

  • kjpou1kjpou1 LUMember, Xamarin Team Xamurai

    Heiko

    You should not need to use CCSpriteBatchNode as can be seen the CCMenuItemImage is supposedly doing things correctly it seems so there must be something that is not being cached correctly. Also, we could be missing something on our side as well. You could look at the CCMenuItemImage code and do something like what it is doing if you would like. If not could you please send a small solution file that demonstrates this inconsistency. Your last example was really precise and thank you for that.

  • HeikofantHeikofant USUniversity ✭✭

    Okay, I found out that the problem was that I have added a CCLayer to my nodes and added the sprite to that layer. That results in 1 draw call for each sprite.

    Attached is my short project that shows that behavior.

    If you uncomment the line in DummyMenuItem.cs, then the draw calls are reduced to 1. Otherwise, they stick at 10.

    By the way, DisplayStats is currently not working for iOS.

    I have another question: I want to use many CCLabel for my interface (I currently have around 18 buttons, each button has two labels and several sprites). Is there any way I can reduce the draw calls that are caused by the labels? Each label increases the draw calls by 1, however most of the labels are similar (e.g. Exercise 1 and Exercise 2).

  • kjpou1kjpou1 LUMember, Xamarin Team Xamurai

    Heifkofant

    Ok I am finally getting to the heart of the problems you are having.

    First, again, want to thank you for the very precise examples. All of them are showing the same thing that you are working a lot with CCLayer. There have been a few changes and CCLayer does not work the way I think you are trying to use it. Even the issues you reported have the same theme. You are using CCLayer for everything. Take a quick look at the following on the Hierarchy.

    I noticed this in most of your samples as well but never thought to mention it because it was not the problem you were having.

    In your code attached in each new CCNode that will hold your button sprite you are adding a CCLayer to that node and then adding all of your graphics to that CCLayer. In the renderer each CCLayer would bump the draw calls up because it is not on the same layer.

    If in your code you do the following:

    public class DummyMenuItem : CCNode
    {
        public CCSprite Background;
        //public CCLayer BackgroundLayer = new CCLayer();
        public DummyMenuItem ()
        {
            this.Background = new CCSprite ("button_background.png");
            //this.BackgroundLayer.AddChild (this.Background);
            //this.AddChild (this.BackgroundLayer);
    
            // FIXME: add the sprite to the node directly instead to the layer.
            this.AddChild(this.Background);
    
        }
    }
    

    The draw calls go to 1 even with 10 buttons drawn. It would be the same for 100 or more as well.

    Maybe this might help:

        #region Button
        class Button : CCNode
        {
    
            CCNode child;
    
            public event TriggeredHandler Triggered;
            // A delegate type for hooking up button triggered events
            public delegate void TriggeredHandler(object sender,EventArgs e);
    
    
            private Button()
            {
                AttachListener();
            }
    
            public Button(CCSprite sprite)
                : this()
            {
                child = sprite;
                AddChild(sprite);
            }
    
    
            public Button(string text)
                : this()
            {
                child = new CCLabel(text, "arial", 16, CCLabelFormat.SystemFont);
                AddChild(child);
    
            }
    
            public string Text
            {
                get { return (child as CCLabel == null) ? string.Empty : ((CCLabel)child).Text; }
                set 
                {
                    if ((child as CCLabel) == null)
                        return;
    
                    ((CCLabel)child).Text = value;
                }
            }
    
            void AttachListener()
            {
                // Register Touch Event
                var listener = new CCEventListenerTouchOneByOne();
                listener.IsSwallowTouches = true;
    
                listener.OnTouchBegan = OnTouchBegan;
                listener.OnTouchEnded = OnTouchEnded;
                listener.OnTouchCancelled = OnTouchCancelled;
    
                AddEventListener(listener, this);
            }
    
            bool touchHits(CCTouch  touch)
            {
                var location = touch.Location;
                var area = child.BoundingBox;
                return area.ContainsPoint(child.WorldToParentspace(location));
            }
    
            bool OnTouchBegan(CCTouch touch, CCEvent touchEvent)
            {
                bool hits = touchHits(touch);
                if (hits)
                    scaleButtonTo(0.9f);
    
                return hits;
            }
    
            void OnTouchEnded(CCTouch  touch, CCEvent  touchEvent)
            {
                bool hits = touchHits(touch);
                if (hits && Triggered != null)
                    Triggered(this, EventArgs.Empty);
                scaleButtonTo(1);
            }
    
            void OnTouchCancelled(CCTouch touch, CCEvent  touchEvent)
            {
                scaleButtonTo(1);
            }
    
            void scaleButtonTo(float scale)
            {
                var action = new CCScaleTo(0.1f, scale);
                action.Tag = 900;
                StopAction(900);
                RunAction(action);
            }
        }
        #endregion
    

    To use the above:

            var btnDoSomething = new Button("DoSomething");
            btnPlay.Triggered += (sender, e) =>
                {
                // place your code here.
                };
    

    This would work for either a textured button or a Text button.

  • kjpou1kjpou1 LUMember, Xamarin Team Xamurai

    For you CCLabel problem. If you are using SystemFont labels there is no way to reduce the draw calls. Each label is it's own entity and do not share a common texture. The only labels that share a common font texture are sprite fonts and bitmap fonts. This is what they were created for.

    There is a backing texture atlas for each font definition that all characters use with bitmap fonts and sprite fonts. They even look the same across platforms whereas each platform using the underlying system calls will draw fonts with a noticeable difference. Even the font sizes will not be the same because of the differences between points, pixels DPI and the such.

  • HeikofantHeikofant USUniversity ✭✭

    Thanks for your answers @kjpou1 !

    I switched over using CCNode insteaf of CCLayer if I want to group several sprites.

    For the CCLabel issue: I am using SpriteFonts, but the draw calls increase by 1 for each label I create.
    Is there any way to manually cache the fonts? The CCSpriteFontCache.SharedInstance has no method at all at the current PCL.

  • kjpou1kjpou1 LUMember, Xamarin Team Xamurai

    Heikofant

    That should be automatic.

    Here is a test I just did with WindowsPhone 8.1 using a spritefont.

    using System;
    using System.Collections.Generic;
    using CocosSharp;
    using Microsoft.Xna.Framework;
    
    namespace SpriteFontCache
    {
        public class IntroLayer : CCLayerColor
        {
    
            public IntroLayer() : base(CCColor4B.Blue)
            {
                // Upper Label
                for (int i = 0; i < 100; i++)
                {
                    string str;
                    str = string.Format("-{0}-", i);
                    var label = new CCLabel(str, "fonts/MarkerFelt", 22, CCLabelFormat.SpriteFont) { Tag = i + 100 };
                    AddChild(label);
    
                    label.AnchorPoint = CCPoint.AnchorMiddle;
                }
    
    
            }
    
            protected override void AddedToScene()
            {
                base.AddedToScene();
    
                // Use the bounds to layout the positioning of our drawable assets
                var s = VisibleBoundsWorldspace.Size;
    
                for (int i = 0; i < 100; i++)
                {
                    var p = new CCPoint(CCMacros.CCRandomBetween0And1() * s.Width, CCMacros.CCRandomBetween0And1() * s.Height);
                    this[i + 100].Position = p;
                }
    
                // Register for touch events
                var touchListener = new CCEventListenerTouchAllAtOnce();
                touchListener.OnTouchesEnded = OnTouchesEnded;
                AddEventListener(touchListener, this);
            }
    
            void OnTouchesEnded(List<CCTouch> touches, CCEvent touchEvent)
            {
                if (touches.Count > 0)
                {
                    // Perform touch handling here
                }
            }
        }
    }
    

    This will create 100 labels. Notice the number of draws is 2 even for all 100. 1 for the labels and 1 for the stats.
    image

  • HeikofantHeikofant USUniversity ✭✭
    edited July 2015

    Thanks @kjpou1 for further investigation.
    I now have found out, that indeed the draw calls are not increased but ONLY IF no CCSprite and CCLabel is simultaneously added to a CCNode.

    My given example produces either 400 draw calls or 1:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    using CocosSharp;
    using DrawCallsBug;
    
    namespace TouchBug
    {
    
        public class MainScene : CCScene
        {
    
            public static MainScene Instance;
    
            public MainScene (CCWindow mainWindow) : base (mainWindow)
            {
                DefaultDesignResolutionSize = new CCSize (2048, 1536);
    
                var dummylayer = new CCLayer();
                this.AddChild(dummylayer);
    
                for(int i=0; i < 200;i++){
    
                    var rand = new Random ();
                    var randPos = new CCPoint (rand.Next (2048) - 1024, rand.Next (1536) - 1536 / 2);
                    dummylayer.AddChild (new DummyNode (){ Position = randPos });
                }
            }
    
    
    
        }
    
        public class DummyNode : CCNode
        {
            public DummyNode () : base ()
            {
                var randNumber = new Random ().Next (200);
                var label = new CCLabel ("N " + randNumber, "Open Dyslexic", 30, CCLabelFormat.SpriteFont); 
                var bg = new CCSprite ("button_background");
    
                //FIXME: either remove the sprite or the label to reduce the draw calls to 1.
                this.AddChild (label);
                this.AddChild (bg);
    
            }
        }
    }
    

    Project can be downloaded at: http://mmorats.com/Heiko/TIL/DrawCallsBug.zip

  • PeterKrusagerPeterKrusager DKMember ✭✭

    This might be relevant. It also appears to happen if you just add a CCSprite to another CCSprite instead of a CCNode. The following will also increase the draw calls by 400 if the value of lag is set to ture:

    protected override void AddedToScene()
    {
        base.AddedToScene();
        var bounds = VisibleBoundsWorldspace;
    
        CCDrawNode node = new CCDrawNode();
        node.DrawSolidCircle(new CCPoint(25, 25), 25, CCColor4B.Red);
        CCRenderTexture r = new CCRenderTexture(new CCSize(50, 50), new CCSize(50, 50), CCSurfaceFormat.Color, CCDepthFormat.Depth24Stencil8);
        r.BeginWithClear(CCColor4B.Transparent);
        node.Visit();
        r.End();
    
        for (int i = 0; i < 200; i++)
        {
            CCSprite s = new CCSprite(r.Texture);
            s.PositionX = CCRandom.GetRandomFloat(25, bounds.Size.Width - 25);
            s.PositionY = CCRandom.GetRandomFloat(25, bounds.Size.Height - 25);
    
            bool lag = true; // If true will result in a huge amount of draw calls.
            CCNode n = lag ? new CCSprite() : new CCNode();
            n.AddChild(s);
            AddChild(n);
        }
    }
    

  • kjpou1kjpou1 LUMember, Xamarin Team Xamurai

    Hey guys

    Draw calls are only batched on the Texture. The renderer tries to keep the texture in memory as long as possible.

    In the CCLabel example above, using a SpriteFont, the same texture is used so under the hood the Renderer can batch all the rendering calls.

    The only way for this to work is that the texture being used is the same for each draw call. Once the texture changes then a new draw call is issued so that the new texture can be moved to memory.

    In Henkofant's example

    --> CCSprite texture to gpu memory
    --> Draw sprite using texture
    --> CCLabel texture to gpu memory
    --> Draw all label sprites using this same texture atlas associated with the label
    --> Next CCSprite used for grouping is the same? No because the last texture was for the sprite font so go to the beginning above and start again.

    So there really is no way for the renderer to keep the same texture in memory because they are different.

    Peter's is kind of a corner case because even though the CCSprite does not have a texture the Renderer still thinks it does because normally a CCSprite is associated with one. In fact the Texture Id works out to zero in this case which would be a different Id than the one used for the render target texture. Could this be considered a bug? Maybe, depending on how you look at it. The definition of a CCSprite is that it has a Texture associated with it even though in this case there is no texture. If someone created a CCSprite without a texture to group other nodes then they should probably rethink the CCSprite and change to a CCNode instead. That would be the correct way.

    That is all CCSpriteBatchNode was, just a way to keep contiguous associated sprites together to use the same texture. Even though it kept them together each CCSpriteBatchNode still created a draw call. Even though there might have been multiple sprites associated with each batch node it still created a draw call for each CCSpriteBatch. Besides doing this it really was not very easy to use with the code becoming quite complicated at times if you wanted to use it as part of a custom component.

    The Renderer tries to work this out dynamically for itself but sometimes needs a little help.

    For example the particle system used a sprite batch to group the particle textures. If you had an explosion particle system and ran it three times it would show three draw calls. With the Renderer all those calls would be batched automatically thus show only one draw call even though there may be 1000 different particles associated. Also assuming that the particle system was placed to be drawn one right after the other in the Scene Graph hierarchy. If for instance you added a particle system to the Scene Graph then a CCSprite and then another particle system using the same texture as the first, you may get 2 draw calls or 3 depending on the order to be drawn as well as the z-order in the scene. This also may change as you add and remove children from the Scene Graph and the Renderer dynamically sorting those to work out if they can be grouped or not.

    Hope this helps.

  • HeikofantHeikofant USUniversity ✭✭

    Thank you @kjpou1 for your explanation.

    So what am I supposed to do for my interface? My fps is dropping too low, the user experience is quite bad. As I said, I have plenty of interface elements with both, sprites and fonts.

  • HeikofantHeikofant USUniversity ✭✭

    Thanks @kjpou1 .
    I could now lower the draw calls to get proper FPS as every of my buttons now increase the draw calls by only 1.

    Do you / does anyone has any further tips in respect to rendering etc? I am quite unfamiliar with graphic performance enhancement.

  • HeikofantHeikofant USUniversity ✭✭
    edited August 2015

    I have now the issue that the RenderTexture.Sprite looks pixelated / shows artifacts.
    If I set RenderTexture.Sprite.IsAntialiased = true then the sprite looks better but is blurry.

    I've read that I have to set the CCCameraProjection to CCCameraProjection.Projection2D, but where do I exactly have to set that?
    Changing the default CCLayer.DefaultCameraProjection= CCCameraProjection.Projection2D had no effect.

  • kjpou1kjpou1 LUMember, Xamarin Team Xamurai

    @Heikofant

    What do you mean by artifacts? Can you post a screen shot?

  • HeikofantHeikofant USUniversity ✭✭

    Here are two screenshots.
    As you can see, the font and the star are pixelated (you can compare the star in a blue menu item with the stars on the right).

  • kjpou1kjpou1 LUMember, Xamarin Team Xamurai

    Are those scaled in any way?

  • HeikofantHeikofant USUniversity ✭✭

    The blue background of the button is scaled, everything else is not.

  • HeikofantHeikofant USUniversity ✭✭

    @kjpou1 Do you have a solution yet?

  • kjpou1kjpou1 LUMember, Xamarin Team Xamurai

    Heikofant

    Sorry to say I do not. Have you tried using different blends for that. Try setting the BlendFunc when drawing. The default is AlphaBlend but maybe what is outline here. This uses a NonPremultiplied blend when drawing.

            drawTriangles = new CCDrawNode();
            drawTriangles.BlendFunc = CCBlendFunc.NonPremultiplied;
    

    Here is some info on PreMultiplied

    http://blogs.msdn.com/b/shawnhar/archive/2009/11/07/premultiplied-alpha-and-image-composition.aspx
    ugly fringes

    These explain the different ways that Pre-Multiplied and NonPremultiplied work with image composition using render targets. Especially with the fringe bleeding.

  • HeikofantHeikofant USUniversity ✭✭

    Hey, I already tried the blendfuncs.
    The result is still pretty crappy (exact the same as on the screenshots I posted above).

    Anyone found a solution how to use sprites + labels with CCRenderTexture and obtain good results?

  • kjpou1kjpou1 LUMember, Xamarin Team Xamurai

    Heikofant

    Can you put together a small project with your assets that is demonstrating that please.

Sign In or Register to comment.