Preserving Aspect Ratio in Fullscreen
by Christopher Andrade · in Torque Game Builder · 01/02/2008 (2:36 am) · 14 replies
For the past two weeks I've been poking around TGB. I was trying to find a solution (by script) to let the user pick any resolution widescreen or fullscreen and still have square pixels. This means putting black borders on the sides of the screen for widescreen users. It occurs to me that this is most likely a source level change rather than a script based one. Does anyone have some firsthand experience with this?
Looking at the macCarbOGLVideo.cc file, I was able to make a small change and get OS 10.5 to properly change resolution without stretching. However this doesn't help the Windows client, and on top of that. I'm not sure how 10.4 and under systems will handle my changes.
I'll post the quick hack if anyone is interested, however I am really looking for some input from users who have already made changes like this.
Looking at the macCarbOGLVideo.cc file, I was able to make a small change and get OS 10.5 to properly change resolution without stretching. However this doesn't help the Windows client, and on top of that. I'm not sure how 10.4 and under systems will handle my changes.
I'll post the quick hack if anyone is interested, however I am really looking for some input from users who have already made changes like this.
#2
Assuming a fullscreen context is always running stretched, I can compensate by shrinking the extents for sceneWindow2d, or by shrinking the camera extents. Unfortunately since this is not something I can safely assume (at least on the Mac build), I have to do some source changes to get reliable behavior.
Thanks for the reply though Benjamin, these are the sorts of issues that I really feel should be archived on Torque Developer Network. Seems a common enough problem for pretty much any kind of game.
01/02/2008 (12:43 pm)
Oh, I see what you mean. That works in a Windowed context (or if the user selects a resolution matching the aspect ratio of the monitor) because we can safely assume the user's desktop is running using square pixels. The problem I've found with TGB so far is that when I request a resolution like 1024x768, I get a stretched screen. However Torque has no idea it's even stretching it. Making this even worse is that sometimes (from my experience, when requesting a resolution close to the monitors native) for example when I select 1600x1200. I get black borders on the sides of the monitor even before I've made any accommodations in my code.Assuming a fullscreen context is always running stretched, I can compensate by shrinking the extents for sceneWindow2d, or by shrinking the camera extents. Unfortunately since this is not something I can safely assume (at least on the Mac build), I have to do some source changes to get reliable behavior.
Thanks for the reply though Benjamin, these are the sorts of issues that I really feel should be archived on Torque Developer Network. Seems a common enough problem for pretty much any kind of game.
#3
With this code block.
I suspect MacCarbShowMenuBar(false); is also unneeded at this point, since I am using Quartz to do the display capture and resolution change. Edit: This assumption was wrong, dead wrong.
Also the first NULL in
Heck, now that I look at it. I'd want to request the current TGB bit depth, not just 32.
01/02/2008 (12:50 pm)
Replace this code block in macCarbOGLVideo.cc// capture main display & go to full screen mode
// TODO: allow frequency selection?
aglSetFullScreen(ctx, newRes.w, newRes.h, 0, 0);
dumpAGLDebugStr();
Con::printf("set AGL fullscreen");
MacCarbShowMenuBar(false);With this code block.
// -----------------------
// <Experiment Code>
// -----------------------
CFDictionaryRef displayMode ;
CFNumberRef number ;
CGDisplayCapture (kCGDirectMainDisplay); // 1
displayMode = CGDisplayBestModeForParametersAndRefreshRate (kCGDirectMainDisplay,
[b]32[/b],newRes.w,newRes.h,[b]NULL[/b],NULL); // 2
CGDisplaySwitchToMode (kCGDirectMainDisplay, displayMode);
// Run the event loop.
[b]//CGReleaseAllDisplays();[/b]
aglSetFullScreen(ctx, 0, 0, 0, 0);
dumpAGLDebugStr();
Con::printf("set AGL fullscreen");
[b]MacCarbShowMenuBar(false);[/b]
// -----------------------
// </Experiment Code>
// -----------------------It doesn't work perfectly yet, because I'm still looking for the correct place to call CGReleaseAllDisplays();I suspect MacCarbShowMenuBar(false); is also unneeded at this point, since I am using Quartz to do the display capture and resolution change. Edit: This assumption was wrong, dead wrong.
Also the first NULL in
[b]32[/b],newRes.w,newRes.h,[b]NULL[/b],NULL); // 2should actually be the refresh rate we want the monitor to run with. I'm not very familiar with selecting refresh rates.
Heck, now that I look at it. I'd want to request the current TGB bit depth, not just 32.
#4
You can easily get automatically the resolution of the user dekstop and set it as the game resolution.
01/02/2008 (4:33 pm)
Quote:Oh, I see what you mean. That works in a Windowed context (or if the user selects a resolution matching the aspect ratio of the monitor) because we can safely assume the user's desktop is running using square pixels. The problem I've found with TGB so far is that when I request a resolution like 1024x768, I get a stretched screen.You're right, I forgot to mention that ^^'
You can easily get automatically the resolution of the user dekstop and set it as the game resolution.
%dekstopres = getDesktopResolution(); setres( getWord(%dekstopres, 0), getWord(%dekstopres, 1), getWord(%dekstopres, 2));It works, at least if your game can manage to still looking good in very high resolutions.
#5
I have a system in place right now that creates a drop down menu with every resolution the user is capable of displaying. They can pick any of them, and the game attempts to adjust the camera to match. That's why I'm trying to find a flexible solution.
I'm hoping to post the scripts once I can get this locked down for Windows and Mac. Gotta keep things cross platform if I can. :)
01/02/2008 (4:47 pm)
That's a possible solution too. Only thing I'd have to work on in that case would be making sure fonts display at a readable size, and making sure the Gui objects scale reasonably.I have a system in place right now that creates a drop down menu with every resolution the user is capable of displaying. They can pick any of them, and the game attempts to adjust the camera to match. That's why I'm trying to find a flexible solution.
I'm hoping to post the scripts once I can get this locked down for Windows and Mac. Gotta keep things cross platform if I can. :)
#7
Oh, and don't rely on CGDisplayBestModeForParametersAndRefreshRate. I've seen it do some silly things on 10.4, e.g. choosing a stretched mode when an unstretched one was available. I'd suggest using CGDisplayAvailableModes and looping through the returned array to find a mode that matches what you want which is/isn't stretched (hint: kCGDisplayModeIsStretched). It'll also work consistently across all hardware and system versions.
01/02/2008 (5:14 pm)
Release the display when you switch out of full screen. I don't think you need to release the display when quitting, though I'll double check what I've done when I get home.Oh, and don't rely on CGDisplayBestModeForParametersAndRefreshRate. I've seen it do some silly things on 10.4, e.g. choosing a stretched mode when an unstretched one was available. I'd suggest using CGDisplayAvailableModes and looping through the returned array to find a mode that matches what you want which is/isn't stretched (hint: kCGDisplayModeIsStretched). It'll also work consistently across all hardware and system versions.
#8
Taking your advice, this is my new displayBestModeForParametersNoStretch() function. Haven't tested it yet, but it should work in theory. I'm not positive if I'm returning the CFDictionaryRef mode correctly though. Fingers are crossed.
01/02/2008 (8:12 pm)
That's exactly what I was looking for Alex, thanks!Taking your advice, this is my new displayBestModeForParametersNoStretch() function. Haven't tested it yet, but it should work in theory. I'm not positive if I'm returning the CFDictionaryRef mode correctly though. Fingers are crossed.
CFDictionaryRef displayBestModeForParametersNoStretch( U32 display, U32 bpp, U32 width, U32 height)
{
// get the display, and the list of all available modes.
CFArrayRef modeArray = CGDisplayAvailableModes(display);
int len = CFArrayGetCount(modeArray);
for(int i=0; i < len ; i++)
{
// get this mode.
CFNumberRef num;
int examWidth, examHeight, examBpp;
bool isStretched;
CFDictionaryRef mode = (CFDictionaryRef) CFArrayGetValueAtIndex(modeArray,i);
// get width
num = CFDictionaryGetValue( mode, kCGDisplayWidth );
CFNumberGetValue(num, kCFNumberLongType,&examWidth);
// get height
num = CFDictionaryGetValue( mode, kCGDisplayHeight );
CFNumberGetValue(num, kCFNumberLongType,&examHeight);
// get bpp
num = CFDictionaryGetValue( mode, kCGDisplayBitsPerPixel );
CFNumberGetValue(num, kCFNumberLongType,&examBpp);
// get stretched
isStretched = CFDictionaryContainsKey( mode, kCGDisplayModeIsStretched);
// If the mode selected is NOT Stretched, and also matches the request height, width, and bpp.
// return the CFDictionaryRef mode.
if(!isStretched && examWidth == width && examHeight == height && examBpp == bpp)
{
return(mode);
}
}
}
#9
Here's the code I used when switching into fullscreen. Note that it's a bit hackish since I didn't want to spend too much time on it.
01/03/2008 (11:32 am)
Hmmm, I release the display when switching out of fullscreen and in OpenGLDevice::shutdown(). I also force the display to black before capturing it (I seem to recall there were visual issues related to capturing while the display was visible), but fade the display back in asynchronously.Here's the code I used when switching into fullscreen. Note that it's a bit hackish since I didn't want to spend too much time on it.
if(fullScreen && platState.captureDisplay)
{
// Reserve the fade hardware
CGDisplayFadeReservationToken token;
CGAcquireDisplayFadeReservation(kCGMaxDisplayReservationInterval, &token);
// Massive hack! Using CGRestorePermanentDisplayConfiguration caused issues when a window was created (it wouldn't be centered)
// so we grab the current mode and restore it using CGDisplaySwitchToMode, which resolves the issue.
if(!prevDisplayDict)
prevDisplayDict = CGDisplayCurrentMode(platState.cgDisplay);
CFDictionaryRef newDispDict = NULL;
// Grab an array of all possible display modes
CFArrayRef arrayOfDisplays = CGDisplayAvailableModes(platState.cgDisplay);
for(CFIndex idx = 0; idx < CFArrayGetCount(arrayOfDisplays); idx++)
{
CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex(arrayOfDisplays, idx);
// Check width
S32 w;
CFNumberRef wNum = (CFNumberRef)CFDictionaryGetValue(dict, kCGDisplayWidth);
CFNumberGetValue(wNum, kCFNumberIntType, &w);
if(w != newRes.w)
continue;
// Check height
S32 h;
CFNumberRef hNum = (CFNumberRef)CFDictionaryGetValue(dict, kCGDisplayHeight);
CFNumberGetValue(hNum, kCFNumberIntType, &h);
if(h != newRes.h)
continue;
// Check bit depth
S32 b;
CFNumberRef bNum = (CFNumberRef)CFDictionaryGetValue(dict, kCGDisplayBitsPerPixel);
CFNumberGetValue(bNum, kCFNumberIntType, &b);
if(b != bpp)
continue;
// Safe for hardware
if(!CFDictionaryContainsKey(dict, kCGDisplayModeIsSafeForHardware))
continue;
// Not interlaced
if(CFDictionaryContainsKey(dict, kCGDisplayModeIsInterlaced))
continue;
// Not television output
if(CFDictionaryContainsKey(dict, kCGDisplayModeIsTelevisionOutput))
continue;
// Not stretched
if(CFDictionaryContainsKey(dict, kCGDisplayModeIsStretched))
continue;
// Found our display mode!
newDispDict = dict;
break;
}
// Fade to black
CGDisplayFade(token, 0.5, kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, 0, 0, 0, true);
// Capture the display (prevents open windows from resizing)
CGDisplayCapture(kCGDirectMainDisplay);
// Switch to our new display mode
if(newDispDict)
CGDisplaySwitchToMode(platState.cgDisplay, newDispDict);
// capture main display & go to full screen mode
// TODO: allow frequency selection?
// 0 for width/height will use the extent of the display mode
aglSetFullScreen(ctx, 0, 0, 0, 0);
dumpAGLDebugStr();
Con::printf("set AGL fullscreen");
MacCarbShowMenuBar(false);
// Fade our view in asynchronously. We'll have rendered at least a few frames by the time this completes.
CGDisplayFade(token, 0.5, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0, 0, 0, false);
// Release the token
CGError err = CGReleaseDisplayFadeReservation(token);
if(err != kCGErrorSuccess)
Con::printf("Releasing fade reservation error %i", err);
}
#10
I didn't think to check kCGDisplayModeIsSafeForHardware or kCGDisplayModeIsInterlaced or even kCGDisplayModeIsTelevisionOutput.
And using CGDisplayFade to hide the resolution change is very ingenious. I'll definitely have to work that in too.
The solution I came up with to stop the window from getting moved when leaving fullscreen was very close to yours.
I ended up storing the current display mode inside platstate.originalMode. Then using CGDisplaySwitchToMode
01/03/2008 (3:23 pm)
Nice to know I was on the right track for some of the code changes, thanks for the code snippet Alex!I didn't think to check kCGDisplayModeIsSafeForHardware or kCGDisplayModeIsInterlaced or even kCGDisplayModeIsTelevisionOutput.
And using CGDisplayFade to hide the resolution change is very ingenious. I'll definitely have to work that in too.
The solution I came up with to stop the window from getting moved when leaving fullscreen was very close to yours.
I ended up storing the current display mode inside platstate.originalMode. Then using CGDisplaySwitchToMode
#11
01/04/2008 (11:56 pm)
Okay, this is what I've got so far in my macCarbOGLVideo.cc file. Ended up adding one function and changing two others. The first edited function is OpenGLDevice::setScreenMode()// create the agl rendering context
ctx = aglCreateContext(fmt, NULL);
dumpAGLDebugStr();
AssertISV( ctx, "We could not create a valid AGL rendering context.");
if(!ctx)
return false;
// format is not needed once we have a context.
aglDestroyPixelFormat(fmt);
// Reserve the fade hardware
CGDisplayFadeReservationToken token;
CGAcquireDisplayFadeReservation(kCGMaxDisplayReservationInterval, &token);
if(fullScreen && platState.captureDisplay)
{
// Find the closest matching display mode for our parameters.
CFDictionaryRef displayMode = myDisplayBestModeForParameters(platState.cgDisplay,bpp,newRes.w,newRes.h, NULL);
// Tell the monitor to get ready
CGDisplayConfigRef config;
CGBeginDisplayConfiguration(&config);
CGConfigureDisplayMode (config, platState.cgDisplay, displayMode);
// Fade to black
CGDisplayFade(token, 0.5, kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, 0, 0, 0, true);
// Capture all displays so that other windows are not resized.
CGCaptureAllDisplays();
// Apply the display configuration to the monitor
CGCompleteDisplayConfiguration (config, kCGConfigureForAppOnly);
// Set up a fullscreen opengl context, but pass in 0,0 for width and height. This
// forces a pass through
aglSetFullScreen(ctx, 0, 0, 0, 0);
dumpAGLDebugStr();
Con::printf("set AGL fullscreen");
MacCarbShowMenuBar(false);
// Fade our view in asynchronously. We'll have rendered at least a few frames by the time this completes.
CGDisplayFade(token, 0.5, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0, 0, 0, false);
}
else
{
// If we are leaving fullScreen mode AND we have the original screen mode saved
if(smIsFullScreen != fullScreen && fullScreen == false)
{
// Fade to black
CGDisplayFade(token, 0.5, kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, 0, 0, 0, true);
// Switch to the original screen mode.
CGRestorePermanentDisplayConfiguration();
// And release the displays.
CGReleaseAllDisplays();
Con::printf("Releasing all displays and restoring monitor to initial settings.");
// Fade our view in asynchronously. We'll have rendered at least a few frames by the time this completes.
CGDisplayFade(token, 0.5, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0, 0, 0, false);
}
// create a window, get it's drawable, and attach our context to the window
bool isFullscreenWindow = (fullScreen && !platState.captureDisplay);
platState.appWindow = MacCarbCreateOpenGLWindow( platState.hDisplay, newRes.w, newRes.h, isFullscreenWindow );
if(!platState.appWindow)
{
Con::errorf("OpenGLDevice::setScreenMode - Failed to create a new window!");
return false;
}
CGrafPtr drawable = GetWindowPort(platState.appWindow);
aglSetDrawable(ctx, drawable);
dumpAGLDebugStr();
Con::printf("Set up AGL windowed support");
}
// Release the fade token
CGError err = CGReleaseDisplayFadeReservation(token);
if(err != kCGErrorSuccess)
Con::printf("Releasing fade reservation error %i", err);
#12
And finally, I added this function
01/05/2008 (12:06 am)
The second function edited was OpenGLDevice::shutdown()Con::printf( "Shutting down the OpenGL display device..." );
// clean up the context, the window, and kill the texture manager
cleanupContextAndWindow();
// Reserve the fade hardware
CGDisplayFadeReservationToken token;
CGAcquireDisplayFadeReservation(kCGMaxDisplayReservationInterval, &token);
bool isFullScreen;
isFullScreen = Con::getBoolVariable("$pref::Video::fullScreen", false);
if(isFullScreen)
{
// Fade to black
CGDisplayFade(token, 0.5, kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, 0, 0, 0, true);
// Switch to the original screen mode.
CGRestorePermanentDisplayConfiguration();
// And release the displays.
CGReleaseAllDisplays();
Con::printf("Releasing all displays and restoring monitor to initial settings.");
// Fade our view in asynchronously. We'll have rendered at least a few frames by the time this completes.
CGDisplayFade(token, 0.5, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0, 0, 0, false);
// Release the fade token
CGError err = CGReleaseDisplayFadeReservation(token);
if(err != kCGErrorSuccess)
Con::printf("Releasing fade reservation error %i", err);
}
}And finally, I added this function
// This function parses through all available display modes, and tries to find an exact match for the values
// passed in. If it can't find an exact match, it uses CGDisplayBestModeForParameters() to find the next closest
// match.
CFDictionaryRef myDisplayBestModeForParameters( int display, int bpp, int width, int height, bool isStretched = false)
{
// get the display, and the list of all available modes.
CFArrayRef modeArray = CGDisplayAvailableModes(display);
CFDictionaryRef mode;
int len = CFArrayGetCount(modeArray);
for(int i=0; i < len ; i++)
{
// get this mode.
CFNumberRef num;
mode = (CFDictionaryRef) CFArrayGetValueAtIndex(modeArray,i);
// Check to see if the width matches. If it doesn't match, kick out to the next mode.
int examWidth;
num = CFDictionaryGetValue( mode, kCGDisplayWidth );
CFNumberGetValue(num, kCFNumberLongType,&examWidth);
if( examWidth != width)
continue;
// Check to see if the height matches. If it doesn't match, kick out to the next mode.
int examHeight;
num = CFDictionaryGetValue( mode, kCGDisplayHeight );
CFNumberGetValue(num, kCFNumberLongType,&examHeight);
if( examHeight != height)
continue;
// Check to see if the bpp matches. If it doesn't match, kick out to the next mode.
int examBpp;
num = CFDictionaryGetValue( mode, kCGDisplayBitsPerPixel );
CFNumberGetValue(num, kCFNumberLongType,&examBpp);
if( examBpp != bpp)
continue;
// Check to see if the mode is safe for the current hardware.
if(!CFDictionaryContainsKey( mode, kCGDisplayModeIsSafeForHardware))
continue;
// Make sure the mode is not an interlaced mode.
if(CFDictionaryContainsKey( mode, kCGDisplayModeIsInterlaced))
continue;
// Make sure the mode is not a television output mode.
if(CFDictionaryContainsKey( mode, kCGDisplayModeIsTelevisionOutput))
continue;
// Retrieve the stretch status of this mode.
bool examStretched = CFDictionaryContainsKey( mode, kCGDisplayModeIsStretched);
// Now check to see if the stretch status returned is the same as requested by isStretched.
if(isStretched == examStretched)
{
// We have found a matching display mode.
return(mode);
}
}
Con::printf("Failed to find a suitable display mode. Using system default.");
return (CGDisplayBestModeForParameters( display, bpp, width, height, NULL));
}
#13
01/05/2008 (12:11 am)
So far I haven't had any problems with this setup. The downside though is that I'm using a few 10.2 and up functions. I'm not sure exactly how I'd get around that, maybe some operating system checks to fall back to the default behavior? Eh, I'll cross that road when I get to it. Thanks for the help everyone!
#14
01/05/2008 (11:26 am)
TGB doesn't work on anything below 10.2.8, and even then you have to #define a few special things to make that work. I wouldn't worry about it.
Torque Owner DragonSix
No need for C++.