7
\$\begingroup\$

Note: In order to keep the question as simple as possible, I am actively changing the body of the question to reflect the current state of affairs, instead of adding stuff at the bottom.

I am implementing interface rotation for my GLES based game for iOS, written in Xamarin.iOS with OpenTK.

I am detecting the rotation by overriding WillRotate, in my UIViewController, and I correctly re-setup all of my projection matrices.

However, the results of drawing a sprite on landscape and portrait modes are slightly different -landscape is a bit blurry-.

This is the test sprite I am using, which is 128x128 to avoid any rounding errors:

Sample image

My test projection matrix is based on a 1024x768 display for an iPad. Even though I won't be scaling the image, I am using point filtering, so I expect rendering to be pixel perfect. The rendering position is (0, 0) once again to avoid any rounding errors. The sprite should fall exactly into the pixel boundaries for both portrait and landscape.

Additionally, in both cases, I'm using the same texture with the same sampler, the same shader, and the same GL state.

This is what happens when I render the sprite in portrait mode. This is pretty much how I expect it to be rendered (magnified 5x):

Portrait original

And this is what happens when I render the same sprite in landscape mode.

Landscape original

My belief is that the framebuffer was created as 768x1024, but when rotated, it is made to fit the 1024x768 screen, squished on the horizontal and stretched in the vertical, and this is what is causing the problem.

If I set OpenTK to create a 1024x1024 framebuffer, the result becomes consistent in portrait and landscape. This is portrait:

Portrait 1024

Notice how there is be no distortion on the vertical, which happens to be 1024 pixels as well. The distortion is limited to the horizontal, where the resolution doesn't match. This is landscape:

Landscape 1024

This time the distortion moved to the vertical, supporting my hypothesis.

Furthermore, creating it at 3072x3072, 3072 being the least common multiple of 1024 and 768, the results are correct both in portrait and landscape:

3072x3072

Notice that this is not a solution, since not only I'm wasting a lot of memory, but also will create inconsistent multisampling artifacts with 3D, as the vertical scaling is different from the horizontal scaling. Additionally, this is not an option for the iPhone 5, whose resolution of 1136x640 would require me to create a framebuffer of 45440x45440.

Since a device rotation could be thought of as a window resize, I think that the framebuffer has to be recreated to match the new size. This is necessary for Windows on Direct3D. I would have to call swapChain.ResizeBuffers() to do this in SharpDX.

I have tried setting AutoResize = true in my iPhoneOSGameView, but then the framebuffer gets clipped as I rotate the interface, and then everything disappears when rotating the interface again.

I'm not doing anything strange, my framebuffer initialization is pretty vanilla:

int scaling = (int)UIScreen.MainScreen.Scale;
DeviceWidth = (int)UIScreen.MainScreen.Bounds.Width * scaling;
DeviceHeight = (int)UIScreen.MainScreen.Bounds.Height * scaling;
Size = new System.Drawing.Size((int)(DeviceWidth), (int)(DeviceHeight));
Bounds = new System.Drawing.RectangleF(0, 0, DeviceWidth, DeviceHeight);
Frame = new System.Drawing.RectangleF(0, 0, DeviceWidth, DeviceHeight);
ContextRenderingApi = EAGLRenderingAPI.OpenGLES2;
AutoResize = true;
LayerRetainsBacking = true;
LayerColorFormat = EAGLColorFormat.RGBA8;

I get inconsistent results when changing Size, Bounds and Frame on my CreateFrameBuffer override. Unfortunately, the documentation is very incomplete, so I have resorted to randomly changing stuff here and there without really knowing what is going on.

There is a similar question which has no answers. However, I don't know if they're experiencing the same problem as I am.

Also, there is a similar concern over at the xamarin forums, specifically mentioning the rotation issue. There seems to be no solution outside of dropping GameView completely.

Is my supposition that recreating the framebuffer is necessary, correct? If so, does anybody know how to do it correctly in OpenTK for Xamarin.iOS?

\$\endgroup\$
  • 2
    \$\begingroup\$ Just a side note, perhaps you could check with a 1px checkerboard pattern image to better see whats going on (upscaling, downsampling, texel shift, etc) \$\endgroup\$ – Kromster says support Monica Jun 4 '14 at 7:24
  • \$\begingroup\$ @Krom: I can do that \$\endgroup\$ – Panda Pajama Jun 4 '14 at 7:27
  • 1
    \$\begingroup\$ what if you don't use device rotation but apply a 90° rotation matrix, will that do the same distortion? \$\endgroup\$ – bogglez Jun 4 '14 at 7:38
  • \$\begingroup\$ @bogglez I can do that too \$\endgroup\$ – Panda Pajama Jun 4 '14 at 8:12
  • \$\begingroup\$ @Krom: I added more information relevant to your comments \$\endgroup\$ – Panda Pajama Jun 5 '14 at 5:14
1
\$\begingroup\$

Yes, the renderbuffer must be recreated when the interface is rotated, and set to the new size.

Recreating it is not difficult. However, the renderbuffer ID is private in iPhoneOSGameView, and only accessible via the Renderbuffer property, which doesn't have a public setter.

Reflection to the rescue! By inspecting iPhoneOSGameView, I found out that it contains a field called renderbuffer that seems to be quite promising as the backing field behind Renderbuffer.

So, when I detect a rotation, I recreate my renderbuffer, and then set it via reflection.

Doing things like this is kinda risky, since I don't really know what else (if anything) I have to change, but so far it looks good.

public void ResizeFrameBuffer(int newWidth, int newHeight)
{
    DeviceWidth = newWidth;
    DeviceHeight = newHeight;

    Size = new System.Drawing.Size(DeviceWidth, DeviceHeight);
    Bounds = new System.Drawing.RectangleF(0, 0, DeviceWidth, DeviceHeight);

    MakeCurrent();

    int renderbuffer = Renderbuffer;

    GL.DeleteRenderbuffers(1, ref renderbuffer);

    GL.BindFramebuffer(FramebufferTarget.Framebuffer, Framebuffer);
    GL.GenRenderbuffers(1, out renderbuffer);
    GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, renderbuffer);
    EAGLContext.RenderBufferStorage((uint)RenderbufferTarget.Renderbuffer, (CAEAGLLayer)Layer);
    GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferSlot.ColorAttachment0, RenderbufferTarget.Renderbuffer, renderbuffer);
    GL.Viewport(0, 0, DeviceWidth, DeviceHeight);
    GL.Scissor(0, 0, DeviceWidth, DeviceHeight);

    var type = typeof(iPhoneOSGameView);
    var renderbufferField = type.GetField("renderbuffer", BindingFlags.NonPublic | BindingFlags.Instance);
    renderbufferField.SetValue(this, renderbuffer);

    Frame = new System.Drawing.RectangleF(0, 0, DeviceWidth, DeviceHeight);
}

iPhoneOSGameView never saw it coming...

\$\endgroup\$
0
\$\begingroup\$

This is now part of OpenTK. iPhoneOSGameView now has a ResizeFrameBuffer method based on @Panda Pajama's answer, which should be called on screen rotation.

https://github.com/opentk/opentk/pull/521

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.