So I have code like this in my game, part of its UI system.
public void Draw(GameTime time, GraphicsContext gfx, RenderTarget2D currentTarget)
{
if (currentTarget == null)
{
Invalidate();
return;
}
if (_isVisible == false)
return;
//gfx.Device.SetRenderTarget(_rendertarget);
//gfx.Device.Clear(Color.Transparent);
//gfx.Device.SetRenderTarget(currentTarget);
if (_invalidated)
{
if (_resized)
{
if (_rendertarget != null)
_rendertarget.Dispose();
_rendertarget = new RenderTarget2D(gfx.Device, _width, _height, false, gfx.Device.PresentationParameters.BackBufferFormat, gfx.Device.PresentationParameters.DepthStencilFormat, 1, RenderTargetUsage.PreserveContents);
if (_userfacingtarget != null)
_userfacingtarget.Dispose();
_userfacingtarget = new RenderTarget2D(gfx.Device, _width, _height, false, gfx.Device.PresentationParameters.BackBufferFormat, gfx.Device.PresentationParameters.DepthStencilFormat, 1, RenderTargetUsage.PreserveContents);
_resized = false;
}
gfx.Device.SetRenderTarget(_userfacingtarget);
gfx.Device.Clear(Color.Transparent);
gfx.BeginDraw();
OnPaint(time, gfx, _userfacingtarget);
gfx.EndDraw();
_invalidated = false;
}
gfx.Device.SetRenderTarget(_rendertarget);
gfx.Device.Clear(Color.Transparent);
gfx.BeginDraw();
gfx.DrawRectangle(0, 0, _userfacingtarget.Width, _userfacingtarget.Height, _userfacingtarget, Color.White, System.Windows.Forms.ImageLayout.Stretch);
gfx.EndDraw();
gfx.Device.SetRenderTarget(currentTarget);
gfx.Device.Clear(Color.Transparent);
gfx.BeginDraw();
gfx.DrawRectangle(0, 0, _rendertarget.Width, _rendertarget.Height, _rendertarget, Color.White, System.Windows.Forms.ImageLayout.Stretch);
gfx.EndDraw();
foreach (var ctrl in _children)
{
if (ctrl.RenderTarget != null)
{
if (ctrl.Control._resized)
{
ctrl.RenderTarget.Dispose();
ctrl.RenderTarget = null;
ctrl.Control.Invalidate();
}
}
if (ctrl.RenderTarget == null)
{
ctrl.RenderTarget = new RenderTarget2D(gfx.Device, ctrl.Control.Width, ctrl.Control.Height, false, gfx.Device.PresentationParameters.BackBufferFormat, DepthFormat.Depth24, 1, RenderTargetUsage.PreserveContents);
ctrl.Control.Invalidate();
}
gfx.Device.SetRenderTarget(ctrl.RenderTarget);
ctrl.Control.Draw(time, gfx, ctrl.RenderTarget);
}
gfx.Device.SetRenderTarget(currentTarget);
foreach (var ctrl in _children)
{
gfx.BeginDraw();
if (Manager.IgnoreControlOpacity)
{
gfx.DrawRectangle(ctrl.Control.X, ctrl.Control.Y, ctrl.Control.Width, ctrl.Control.Height, ctrl.RenderTarget, Color.White, System.Windows.Forms.ImageLayout.Stretch);
}
else
{
gfx.DrawRectangle(ctrl.Control.X, ctrl.Control.Y, ctrl.Control.Width, ctrl.Control.Height, ctrl.RenderTarget, Color.White * ctrl.Control.Opacity, System.Windows.Forms.ImageLayout.Stretch);
}
gfx.EndDraw();
}
}
Basically, my user interface system is similar in structure to Windows Forms. Each UI element has its own set of children, a parent element (null if it’s a top-level control), a UI manager instance which allows the control to access things like the currently focused element, the screen size, config settings for the UI, etc.
I’ve heavily optimized my UI engine. Each control has a set of render targets, all three of which match the width and height of the control. One, the front buffer, is where user painting is rendered to. It is only modified if the control is invalidated. Thus, we only need to run user painting code “every now and then” instead of every frame. The back buffer render target is mofified every frame and is where the front buffer is rendered to.
Then there’s the parent render target, it’s where the back buffer is rendered to, except the parent control (or the UI manager itself) manages it rather than the control being rendered.
That’s where our problem arises. Those two foreach statements that operate on the _children
list are where the issue really is. Specifically, the first foreach block.
So I have a number of checks in there to make sure the parent buffer is okay to render to - things like “is it null? does it match the width and height of the control?” etc. If a check fails, the render target is regenerated so that all the checks will pass. Once the render target is ready, I call the “Draw” method on the control, telling it to render to the parent buffer. Easy-peasy. And, it works. Because when I run my game and open my WIP settings menu, I get a UI that looks like this.
Simple! You get a text block that tells you its opacity, and two buttons - one with an image adornment, one without. Except, that’s not what the UI is supposed to look like. Layout-wise, it’s fine. Everything’s in the right spot…but it’s…missing something.
Specifically, it’s missing a fourth UI element - a picture box - that has its texture set to my game’s logo. And it is CERTAINLY registered as a child of that window. You can see this if you draw a red box under each child element:
Notice how the red box outlines the transparent/translucent areas of each child element? And notice how there’s a big solid red box right underneath the “This text is 50% transparent” text?
That’s where my picture box is! It’s there! The UI engine KNOWS it’s there! So what the heck’s going on here!? Well, it seems that every child past the first three simply do not render to their parent buffers. They render their back and front buffers just fine - but their parent buffers end up fully transparent. And you can see this by simply changing the order in which each control is added to their parent.
Note: My window border’s caption buttons are just hitboxes with the window border rendering the textures on its own. They’re not affected by the bug. That’s normal.
I moved the button with the image so it is the fourth child of the window, and as you can see the picture box now renders just fine, and the button with the image is now a red box. The heck?
Let’s try adding a fifth UI element. I’ll add a text control just below the picture box.
Alright…so…guess the issue’s changed. The text below the picture renders just fine, and it renders AFTER that button that ISN’T rendering.
So what’s the pattern!? I KNOW now that it’s not EVERY control after the third that’s affected by the bug… could it only be the fourth? Could it be every fourth? Who knows? I don’t.
But I do know that, forgive my language, this bug is pissing me off. And I want to get it fixed. And I’m out of ideas. I had a similar bug where only ONE child would render, and I fixed that by moving the parent buffer checks to the Draw() method. That fixed it, but caused this bug.
Edit 1: It’s only the fourth.
Only the fourth render target is affected by the bug. What’s going on here!?