[Solved] Strange bug with RenderTargets in my game.

Instead of typing a bunch of paragraphs, I may as well just post the video of the bug here.


(Yes, before you ask, I know I said I refuse to use Windows. But, I never said anything about Windows 7, which is far better than any Windows version this decade has brought us.)

Anyway, here is the code for the UIManager class which deals with rendering top level controls (and the red background in the game’s debug scene):

//EDITOR'S NOTE: GraphicsContext is merely a wrapper for SpriteBatch and GraphicsDevice so that said objects can be passed around as a single concise object.
//
//It also has built-in scissortesting and methods for drawing rectangles, circles, lines and text.

        public void OnFrameDraw(GameTime time, GraphicsContext ctx)
        {
            ctx.Batch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied,
                    SamplerState.LinearWrap, DepthStencilState.Default,
                    RasterizerState.CullNone);
            ctx.Clear(Color.Red);
            ctx.Batch.End();
            foreach (var ctrl in _topLevels)
            {
                ctx.Device.SetRenderTarget(ctrl.RenderTarget);
                ctrl.Control.Draw(time, ctx, ctrl.RenderTarget);
                
                ctx.Device.SetRenderTarget(_plexgate.GameRenderTarget);
                ctx.Batch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied,
        SamplerState.LinearWrap, DepthStencilState.Default,
        RasterizerState.CullNone);
                ctx.DrawRectangle(ctrl.Control.X, ctrl.Control.Y, ctrl.Control.Width, ctrl.Control.Height, ctrl.RenderTarget, Color.White);
                ctx.Batch.End();

            }
            ctx.Batch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied,
        SamplerState.LinearWrap, DepthStencilState.Default,
        RasterizerState.CullNone);

            var fps = Math.Round(1 / time.ElapsedGameTime.TotalSeconds);
            ctx.DrawString($"FPS: {fps}", 0, 0, Color.White, new System.Drawing.Font("Lucida Console", 12F), TextAlignment.TopLeft);
            ctx.Batch.End();
        }

        public void OnGameUpdate(GameTime time)
        {
            var mouse = Mouse.GetState();
            foreach(var ctrl in _topLevels)
            {
                var w = ctrl.Control.Width;
                var h = ctrl.Control.Height;
                bool makeTarget = false;
                if (ctrl.RenderTarget == null)
                    makeTarget = true;
                else
                {
                    if(ctrl.RenderTarget.Width != w || ctrl.RenderTarget.Height != h)
                    {
                        makeTarget = true;
                    }
                }
                if (makeTarget)
                {
                    ctrl.RenderTarget = new RenderTarget2D(_plexgate.GraphicsDevice, ctrl.Control.Width, ctrl.Control.Height, false, _plexgate.GraphicsDevice.PresentationParameters.BackBufferFormat, _plexgate.GraphicsDevice.PresentationParameters.DepthStencilFormat, 1, RenderTargetUsage.PreserveContents);
                    ctrl.Control.Invalidate();
                }
                ctrl.Control.SetTheme(_thememgr.Theme);
                ctrl.Control.Update(time);
            }

            //Propagate mouse events.
            foreach(var ctrl in _topLevels)
            {
                if (ctrl.Control.PropagateMouseState(mouse.LeftButton, mouse.MiddleButton, mouse.RightButton))
                    break;
            }
        }

And the rendering + updating code for Control - responsible for the actual rendering of the window border - and where the AccessViolationException is thrown.

//_rendertarget = an instance member of the Control class. Used as a way of double-buffering the control itself to improve performance.
        public void Draw(GameTime time, GraphicsContext gfx, RenderTarget2D currentTarget)
        {
            if (_invalidated)
            {
                if (_resized)
                {
                    _rendertarget = new RenderTarget2D(gfx.Device, _width, _height, false, gfx.Device.PresentationParameters.BackBufferFormat, gfx.Device.PresentationParameters.DepthStencilFormat, 1, RenderTargetUsage.PreserveContents);
                    //_resized = false;
                }
                int lastw = gfx.Width;
                int lasth = gfx.Height;
                gfx.Width = _width;
                gfx.Height = _height;
                gfx.Device.SetRenderTarget(_rendertarget);
                gfx.Batch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied,
                    SamplerState.LinearWrap, DepthStencilState.Default,
                    RasterizerState.CullNone);
                OnPaint(time, gfx, _rendertarget);
                gfx.Batch.End();
                gfx.Width = lastw;
                gfx.Height = lasth;
                _invalidated = false;
            }
            foreach (var ctrl in _children)
            {
                int lastw = gfx.Width;
                int lasth = gfx.Height;
                int lastx = gfx.X;
                int lasty = gfx.Y;
                gfx.Width = ctrl.Control._width;
                gfx.Height = ctrl.Control._height;
                gfx.Device.SetRenderTarget(ctrl.RenderTarget);
                ctrl.Control.Draw(time, gfx, ctrl.RenderTarget);
                gfx.Device.SetRenderTarget(_rendertarget);
                gfx.Batch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied,
    SamplerState.LinearWrap, DepthStencilState.Default,
    RasterizerState.CullNone);
                gfx.DrawRectangle(ctrl.Control.X, ctrl.Control.Y, ctrl.Control.Width, ctrl.Control.Height, ctrl.RenderTarget, Color.White);
                gfx.Batch.End();

                gfx.Width = lastw;
                gfx.Height = lasth;
                gfx.X = lastx;
                gfx.Y = lasty;
            }
            gfx.Device.SetRenderTarget(currentTarget);
            gfx.Batch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied,
SamplerState.LinearWrap, DepthStencilState.Default,
RasterizerState.CullNone);

            gfx.DrawRectangle(0, 0, _rendertarget.Width, _rendertarget.Height, _rendertarget, Color.White);
            gfx.Batch.End();
            /*try
            {
                byte[] data = new byte[(_rendertarget.Width * 4) * _rendertarget.Height];
                _rendertarget.GetData<byte>(data); //accessviolation occurs here.
            }
            catch { }*/
        }

        public void Update(GameTime time)
        {
            if (_rendertarget == null)
            {
                _invalidated = true;
                _resized = true;
            }
            //Poll the mouse state.
            var mouse = Mouse.GetState();
            //For toplevels, set mouse input loc directly.
            int _newmousex = 0;
            int _newmousey = 0;
            if (Parent == null)
            {
                _newmousex = X + mouse.X;
                _newmousey = Y + mouse.Y;
            }
            //For controls with parents, poll mouse information from the parent.
            else
            {
                _newmousex = X + Parent._mousex;
                _newmousey = Y + Parent._mousey;
            }
            if(_newmousex != _mousex || _newmousey != _mousey)
            {
                _mousex = _newmousex;
                _mousey = _newmousey;
                MouseMove?.Invoke(this, new Vector2(_newmousex, _newmousey));
                _invalidated = true;
            }
            OnUpdate(time);
            foreach (var child in _children)
            {
                bool makeTarget = false;
                if (child.RenderTarget == null)
                    makeTarget = true;
                else
                {
                    if (child.RenderTarget.Width != child.Control.Width || child.RenderTarget.Height != child.Control.Height)
                        makeTarget = true;
                }
                if (makeTarget)
                {
                    if(_rendertarget != null)
                    {
                        child.RenderTarget = new RenderTarget2D(_rendertarget.GraphicsDevice, child.Control.Width, child.Control.Height, false, _rendertarget.Format, _rendertarget.DepthStencilFormat, 1, RenderTargetUsage.PreserveContents);
                        child.Control.Invalidate();
                    }
                }
                child.Control.Update(time);
            }
        }

I’m running MonoGame 3.6, on Windows 7 (64-bit). This is a DesktopGL project.

Any help would be greatly appreciated!

Also, as said in the video, the rendertarget that’s throwing the error seems fine when I inspect it in the Locals window. Weird.

According to your debug output, _resized is true and I don’t see you setting it to false anywhere.

I’d suspect you’re creating render-targets all the time without disposing of them. Eventually, you’re going to nuke if that’s the case.

YEP, that was what did it. I wrote that code around midnight the other night and just for a ha, I uncommented the line that sets _resized to true…and that fixed it. Framerate shot back up to solid 60, and the bug died.

Tip of advice: If you stay up 'til midnight, extensively review your code the next morning when you’re actually awake. :stuck_out_tongue:

At ~20 fps you were allocating ~26 mb per second. In a minute you’d be well past 1 gb.

You roughly started the program at 1:54 and it threw at 2:57. I’m going to guess you have 4gb of ram.

VS will take around 900mb, w/e browser you have going will take 250mb, the OS core will take 300mb, misc background programs will take 600mb, then the debugger attached heap takes its toll and your program eats 25% more than usual, and lastly your recording program could easily eat past 1gb.

That leaves you with enough space to eat everything the APU can touch in less than a minute (20fps wasn’t constant), and off into swap land you go.

Right or wrong? (It’s not judgemental, I do most of my work on a potato Surface pro 1st gen - just want to know if I crunched it right)

2 Likes

Nope, you’re 4GB short. :stuck_out_tongue:

Not sure what most of the apps running on my system (I had Chrome going so I highly doubt it took anything less than 1gb) were taking, but since uncommenting the _resized=true; in Control.Draw() fixed it - I’d say it was a memory leak, so you nailed that.

RT data is not stored in RAM by MG, only on the GPU in VRAM. I think you’d get an OutOfMemoryException if it was RAM that was full.

Yeah, but keep in mind that I’m on an APU. Pretty sure that the onboard Radeon R7 that’s embedded into the APU would share the RAM with the CPU. Not sure though. Not entirely sure how the heck AMD APUs work.

Oh, I missed that! :stuck_out_tongue:

The APU is probably only being allowed to use 1gb, should be configurable somewhere. That’s reasonable and the only real reason to go higher (it’s not going to improve perf. really) is when debugging where all of the GPU resources of the VS graphics-debugger / gDebugger will eat on top of your planned usage. They basically set up hundreds of “staged” render-targets for capturing buffer states.