libsdl-org / SDL

Simple Directmedia Layer
https://libsdl.org
zlib License
9.36k stars 1.74k forks source link

Setting content orientation independently from the actual framebuffer orientation #7627

Open hikari-no-yume opened 1 year ago

hikari-no-yume commented 1 year ago

On mobile devices with screens that scan out in portrait (which is most of them), the best practice is for games to output a portrait framebuffer, even if the actual content of it is intended to be displayed in landscape. This is to avoid a performance penalty if the OS has to composite a rotated version of the framebuffer before displaying it. If you search online for “prerotation” you'll see various references to this.

On iOS, you can just output a portrait framebuffer and tell the OS that the status bar etc should be displayed in landscape. The old way to do this was [UIApplication setStatusBarOrientation:]; I'm not sure how you do this on modern iOS. I assume Android has an equivalent.

It seems like SDL infers the orientation of the content from the orientation of the framebuffer, though, so you can't avoid this performance penalty without having the status bar etc on the wrong side of the screen? Please tell me if I'm wrong. I notice SDL_HINT_QTWAYLAND_CONTENT_ORIENTATION exists, which seems like it could be what I want, but there's no iOS or Android equivalent?

Vaguely related: https://github.com/libsdl-org/SDL/issues/6278.

For context, my current project uses SDL 2 and emulates very old iOS games, so I've had to become familiar with all things orientation.

icculus commented 1 year ago

How big of a performance hit are we talking here? I'm surprised this cost isn't effectively hidden by even a modest GPU.

That being said, we could probably adapt the SDL hint that forces iOS into portrait/landscape mode to offer a hybrid setting of portrait-but-adjust-the-status-bar.

Also, if creating the SDL window with SDL_WINDOW_BORDERLESS (which hides the status bar on iOS), you could probably just force it to portrait mode and be done without any further changes to SDL, under the assumption that the game will manage rotation of drawing and touch coordinates. But if you want the status bar, we'll have to tweak SDL here.

hikari-no-yume commented 1 year ago

How big of a performance hit are we talking here?

I haven't measured it myself, but Google's documentation mentions “1-3ms hits to frametimes” and that if this is avoided “GPU frequency drops by 40%”, though this depends on the particular device. The penalty is less on some devices.

I'm surprised this cost isn't effectively hidden by even a modest GPU.

The GPU is actually where the cost comes from: inserting a GPU render pass to create a rotated version of the framebuffer. It's not particularly hard for the GPU to do, but it costs time and memory bandwidth.

On some devices, the display processor (DPU) can do rotation itself, skipping the GPU cost, but there's still going to be some overhead because you're reading a linear framebuffer in the wrong direction.

That being said, we could probably adapt the SDL hint that forces iOS into portrait/landscape mode to offer a hybrid setting of portrait-but-adjust-the-status-bar.

Would that lock the app into a single orientation until the app exits? What iOS provides is dynamic, so the app can change orientation at runtime if it wants to. (For my application this would be useful, but I'll freely admit my application is unusual.)

hikari-no-yume commented 1 year ago

Oh, right, an important caveat is that some OpenGL ES drivers will silently do this “pre-rotation” behind the scenes, so that's another way that it can be hidden. My understanding is that because Vulkan is more explicit/low-level, the drivers can't do that trick there.

hikari-no-yume commented 1 year ago

The behaviour on Android turns out to be weirder than I was expecting. It seems like, if I request a fullscreen window, it's always considered portrait, regardless of what width and height I ask for. That's arguably good for performance, but this will mean that swipe gestures to bring down notifications etc will be on the wrong edge of the screen, right?

hikari-no-yume commented 1 year ago

Oh, it seems I misunderstood what SDL_HINT_ORIENTATIONS does! When I read the relevant Android code it became much clearer. If I use that, it seems I can force a certain orientation, both in fullscreen and in windowed mode. Alas, it doesn't let a portrait framebuffer be considered landscape…

hikari-no-yume commented 1 year ago

Ah wait, it actually does allow that too! Okay, I think this is just a documentation issue then, at least on Android.

I want to suggest though that for SDL 3, this shouldn't be a hint.

slouken commented 1 year ago

Do you have a suggested change to improve the documentation?

hikari-no-yume commented 1 year ago

Currently it's:

/**
 *  \brief  A variable controlling which orientations are allowed on iOS/Android.
 *
 *  In some circumstances it is necessary to be able to explicitly control
 *  which UI orientations are allowed.
 *
 *  This variable is a space delimited list of the following values:
 *    "LandscapeLeft", "LandscapeRight", "Portrait" "PortraitUpsideDown"
 */
#define SDL_HINT_ORIENTATIONS "SDL_IOS_ORIENTATIONS"

I would add something like:

This hint does two things:

  • Auto-rotation is limited to the rotations in the list. For example, if the window is landscape and you don't set this hint, turning the device upside-down will flip between LandscapeLeft and LandscapeRight. If the list only contains one orientation, the rotation will be locked.
  • Normally, SDL guesses the desired orientation from the window dimensions: width < height is portrait, width > height is landscape. If this hint is set, SDL's guess is constrained by the orientations in the list. For example, you can use this to have a fullscreen window with width < height be considered landscape. You might want to do this to optimize the performance of a landscape game on smartphones with portrait displays, without affecting the direction the user must swipe to bring up OS controls.

By the way, I discovered that I can't make SDL change the orientation dynamically and have the window always remain in portrait. That's inconvenient for me but probably not an issue for most games, my app is a very special case.

hikari-no-yume commented 1 year ago

Oh, but one big caveat with that documentation suggestion is I only know with any certainty what SDL2 does on one device running one version of Android. Assuming Android versions are consistent, the main thing to check is whether it behaves the same on iOS?

hikari-no-yume commented 1 year ago

Oh okay so I can't dynamically rotate the screen at all in fullscreen, it seems. :(

hikari-no-yume commented 1 year ago

Okay, actually it is possible to dynamically rotate the screen, but it's incredibly hacky:

SDL_SetHint(SDL_HINT_ORIENTATIONS, new_orientation);
// Make window no longer fullscreen so resizableness can be changed
SDL_SetWindowFullscreen(window, 0);
// Make window resizable, which triggers Android_JNI_SetOrientation, which applies new the hint 
SDL_SetWindowResizable(window, SDL_TRUE);
// Restore old resizeable (maybe could be skipped)
SDL_SetWindowResizable(window, SDL_FALSE);
// Return to fullscreen
SDL_SetWindowFullscreen(window, 0);

Obviously nobody should actually do that (though I will anyway). It would be nice if there were a function like SDL_SetWindowOrientation instead. It seems like that would be simple to add? The weird sequence of commands there is only needed because of how awkwardly orientation changes fit into SDL 2's desktop-like window assumptions (fullscreen windows can't change size, etc).