RetinaHelper: wxGLCanvas and hi-res (retina) on os x

If you have a cool piece of software to share, but you are not hosting it officially yet, please dump it in here. If you have code snippets that are useful, please donate!
Post Reply
andreasstahl
In need of some credit
In need of some credit
Posts: 5
Joined: Wed Jan 09, 2013 10:04 am

RetinaHelper: wxGLCanvas and hi-res (retina) on os x

Post by andreasstahl »

While Wx 2.9.4 in general works well with retina displays (if the NSPrincipalClass key is set in the apps plist, see http://wiki.wxwidgets.org/WxMac-specifi ... ay_support), the case of a wxGLCanvas being displayed on a retina display is not handled correctly at the moment. That's because the "NSView OpenGL Additions" category methods (most importantly setWantsBestResolutionOpenGLSurface:) are not yet exposed by wxWidgets in pure C++. Also, to adapt to a change in display backing resolution the view needs to react to NSWindowDidChangeBackingPropertiesNotification, and be able to determine the backingScaleFactor for general resize events. I developed a workaround for those issues, by introducing a small helper class called RetinaHelper. It's not very elegant, and I'm still grasping with its usage of the pimpl-idiom, but I think it does the job quite nicely.

We need three files:
* RetinaHelper.h, which is your standard c++ header
* RetinaHelperImpl.h, which is the header for the objective-c++ part of the class.
* RetinaHelperImpl.mm, the objective-c++ "meat".

In RetinaHelper.h we declare the c++-class and it's methods that we need exposed to our c++ code:

Code: Select all

// file RetinaHelper.h
#ifndef RetinaHelper_h
#def RetinaHelper_h

class wxWindow;

class RetinaHelper
{
public:
	RetinaHelper(wxWindow* window);
	~RetinaHelper();
	
	void setViewWantsBestResolutionOpenGLSurface(bool value);
	bool getViewWantsBestResolutionOpenGLSurface();
	float getBackingScaleFactor();

private:
	wxWindow* _window;
	void* _self; // pointer to obj-c++ implementation
};
#endif // RetinaHelper_h
In RetinaHelperImpl.h, we declare the objective-c++ parts of our helper class:

Code: Select all

// file RetinaHelperImpl.h
#import "RetinaHelper.h"
#import <Cocoa/Cocoa.h>

// forward declaration
class wxEvtHandler;


@interface RetinaHelperImpl : NSObject
{
	NSView *view;
	wxEvtHandler* handler;
}
// designated initializer
-(id)initWithView:(NSView *)view handler:(wxEvtHandler *)handler;
-(void)setViewWantsBestResolutionOpenGLSurface:(BOOL)value;
-(BOOL)getViewWantsBestResolutionOpenGLSurface;
-(float)getBackingScaleFactor;
@end
RetinaHelperImpl.mm is where the magic happens. Going into it, you may ask, how do we obtain the NSView or the wxEvtHandler from our wxGLCanvas to initialize our objective c++ object? The latter is trivial: wxGLCanvas is a subclass of wxWindow, wxWindow is a subclass of wxEvtHandler. We could just pass it in, yet it seems to be convention to call its method GetEventHandler() to obtain it. Obtaining the NSView however is theoretically a bit more involved, in practice just as easy: the method GetHandle() in wxWindow returns an object of type WXWidget, which through a cascade of typedefs is resolved to a pointer to the NSView "peer" of the wxWindow.

Code: Select all

// file RetinaHelper.mm

#import "RetinaHelperImpl.h"
#import <OpenGL/OpenGL.h>

#import "wx/window.h"

@implementation RetinaHelperImpl

RetinaHelper::RetinaHelper(wxWindow* window) :
	_window(window)
{
	_self = nullptr;
	_self = [[RetinaHelperImpl alloc] initWithView:window->GetHandle() handler:window->GetEventHandler()];
}

RetinaHelper::~RetinaHelper()
{
  [_self release];
}

void RetinaHelper::setViewWantsBestResolutionOpenGLSurface (bool aValue)
{
	[(id)_self setViewWantsBestResolutionOpenGLSurface:aValue];
}

bool RetinaHelper::getViewWantsBestResolutionOpenGLSurface()
{
	return [(id)_self getViewWantsBestResolutionOpenGLSurface];
}

float RetinaHelper::getBackingScaleFactor()
{
	return [(id)_self getBackingScaleFactor];
}


-(id)initWithView:(NSView *)aView handler:(wxEvtHandler *)aHandler
{
	self = [super init];
	if(self)
	{
		handler = aHandler;
		view = aView;
		// register for backing change notifications
		NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
		if(nc){
			[nc addObserver:self
						 selector:@selector(windowDidChangeBackingProperties:)
								 name:NSWindowDidChangeBackingPropertiesNotification
							 object:nil];
		}
	}
	return self;
}

-(void) dealloc
{
	// unregister from all notifications
	NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
	if(nc){
		[nc removeObserver:self];
	}
	[super dealloc];
}

-(void)setViewWantsBestResolutionOpenGLSurface:(BOOL)value
{
	[view setWantsBestResolutionOpenGLSurface:value];
}

-(BOOL)getViewWantsBestResolutionOpenGLSurface
{
	return [view wantsBestResolutionOpenGLSurface];
}

-(float)getBackingScaleFactor
{
	return [[view window] backingScaleFactor];
}

- (void)windowDidChangeBackingProperties:(NSNotification *)notification {
	NSWindow *theWindow = (NSWindow *)[notification object];
	if(theWindow == [view window])
	{
		CGFloat newBackingScaleFactor = [theWindow backingScaleFactor];
		CGFloat oldBackingScaleFactor = [[[notification userInfo]
																			objectForKey:@"NSBackingPropertyOldScaleFactorKey"]
																		 doubleValue];
		if (newBackingScaleFactor != oldBackingScaleFactor) {
			// generate a wx resize event and pass it to the handler's queue
			wxSizeEvent *event = new wxSizeEvent();
			// use the following line if this resize event should have the physical pixel resolution
			// but that is not recommended, because ordinary resize events won't do so either
			// which would necessitate a case-by-case switch in the resize handler method.
			// NSRect nsrect = [view convertRectToBacking:[view bounds]];
			NSRect nsrect = [view bounds];
			wxRect rect = wxRect(nsrect.origin.x, nsrect.origin.y, nsrect.size.width, nsrect.size.height);
			event->SetRect(rect);
			event->SetSize(rect.GetSize());
			handler->QueueEvent(event);
		}
	}
}
@end
Suppose you have a wxFrame subclass with a wxGLCanvas* _canvas. Also, you need to have some kind of OnSize(wxSizeEvent &event) handler method bound to the wxGLCanvas in which you call the glViewport() method. What happens now? Install our helper, of course! The following is untested prototypical code, I work with a different OpenGL architecture (OpenSceneGraph) and I don't have the time to write a complete demonstrator right now. On request I will do so, later.

But, as of now here's the header file for a Retina-ready 3D-Viewer class:

Code: Select all

// file My3DFrame.h
#include "wx/frame.h"

class RetinaHelper;
class wxGLCanvas;

class My3DFrame :: public wxFrame 
{
  My3DFrame(wxWindow* parent);
//
// additional code omitted
//
  void OnCanvasSize(wxSizeEvent& event);

private:

  wxGLCanvas* _pCanvas;

#ifdef __APPLE__
  RetinaHelper* _pRetinaHelper;
#endif

  // for use with glViewport. 
  int _width;
  int _height;
  bool _viewportNeedsRefresh;
};
And the corresponding interesting bits of implementation:

Code: Select all

#include "My3DFrame.h"
#include "wx/glcanvas.h"

My3DFrame::My3DFrame(wxWindow *parent)
{
  //... initialisation code for glCanvas omitted ...
  // Canvas Resize
  _pCanvas->Bind(wxEVT_SIZE, &My3DFrame::OnCanvasSize, this);
  // on os x: install retina helper
#ifdef __APPLE__
  _pRetinaHelper = new RetinaHelper(_pCanvas);
  _retinaHelper->setViewWantsBestResolutionOpenGLSurface(true);
#endif
}

My3DFrame::~My3DFrame()
{
#ifdef __APPLE__
  delete _pRetinaHelper;
#endif
}

My3DFrame::OnCanvasResize(wxSizeEvent &event)
{
  wxSize newSize = event.GetSize();
  _width = newSize.GetWidth();
  _height = newSize.GetHeight();
#ifdef __APPLE__
  float scaleFactor = _retinaHelper->getBackingScaleFactor();
  _width *= scaleFactor;
  _height *= scaleFactor;
#endif
  // flag to reset glViewport on next render tick, I'm sure you'll have something similar.
  _viewportNeedsRefresh = true;
  event.Skip();
}
Build and run it and (hopefully) you will now be able to drag your window across different displays and the 3d content will be rendered at pixel resolution. I hope wxWindows will implement void MacSetWantsBestResolutionOpenGLSurface(bool), MacGetBackingScaleFactor() and handle the rescaling events on different displays.

Edit: fixed a memory leak from not calling release on _self in the dtor of RetinaHelper.
Last edited by andreasstahl on Wed Jan 09, 2013 5:02 pm, edited 1 time in total.
User avatar
T-Rex
Moderator
Moderator
Posts: 1248
Joined: Sat Oct 23, 2004 9:58 am
Location: Zaporizhzhya, Ukraine
Contact:

Re: RetinaHelper: wxGLCanvas and hi-res (retina) on os x

Post by T-Rex »

Useful approach, thanks for sharing.
orson
In need of some credit
In need of some credit
Posts: 1
Joined: Sat May 16, 2015 12:15 am

Re: RetinaHelper: wxGLCanvas and hi-res (retina) on os x

Post by orson »

Hi andreasstahl,

Thank you for sharing the solution, it works really great! We faced exactly the same issue in a free & open source project, therefore I wanted to ask - would you allow others to use your code under GPLv2+ license?
Post Reply