Page 1 of 1

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

Posted: Wed Jan 09, 2013 2:04 pm
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.

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

Posted: Wed Jan 09, 2013 4:55 pm
by T-Rex
Useful approach, thanks for sharing.

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

Posted: Sat May 16, 2015 12:32 am
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?