This wasn't a SMOP. Not even close. If you've read my book, you know
that I use SMOP to mean Subject Matter Oriented Programming. The
Hacker's Dictionary defines SMOP as a Small Matter of Programming.
To me, they are one in the same. If you focus on the subject matter,
it truly is a small matter of programming. Way too much software is
written with the solution in mind.
I'm beat. I lost four days trying to figure out how to write 150
lines of Objective-C code. Objective-C is easy even though I hadn't
written an Objective-C app before this week. What killed me was the
fact that all the examples on developer.apple.com are wrong, and
worse, they're badly written. I'm happy I finally figured out my
little 150 line program. Apologies to the other bivions who had to
listen to me rant all week. Now it's your turn. :-)
Here's the correct code that had me stumped:
GLint h = height;
while (--h > 0) {
bcopy(&data[h * bytesPerRow], &flipped[(height - h) * bytesPerRow],
bytesPerRow);
}
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glPixelStorei(GL_UNPACK_ROW_LENGTH, bytesPerRow / (bitsPerPixel / 8));
glDrawPixels(width, height, format, GL_UNSIGNED_BYTE, flipped);
There's other code that was easy. For example, I was easily able to
go full screen and hide the mouse with this:
if (CGCaptureAllDisplays() != CGDisplayNoErr)
return nil;
[ctx setFullScreen];
[ctx makeCurrentContext];
CGDisplayHideCursor(kCGDirectMainDisplay);
CGAssociateMouseAndMouseCursorPosition(FALSE);
Yes, this list is supposed to be about Perl, but it's mostly about XP,
and XP is about code. There are lessons to be learned in reading
other people's code, and that's what this message is about.
Fail Fast. Never assume. That's true in *any* application. Code
that fails slowly is bad. That's the first problem with the Apple
examples. Here's the bit that bit me:
// Upload the texture bitmap. (We assume it has power-of-2 dimensions.)
When we assume at bivio, we write it big, e.g.
// ASSUME: Image has power-of-2 dimensions
This tells the reader that the coder has been lazy, and all bets are
off. All Apple demo apps assume that the images they load are
power-of-2 dimensions. If you load something else in them, they look
like a 1950s TV set gone haywire, or in my case, they simply don't
display anything. Here's the full context:
// Upload the texture bitmap. (We assume it has power-of-2 dimensions.)
width = [self pixelsWide];
height = [self pixelsHigh];
hasAlpha = [self hasAlpha];
bitmapData = [self bitmapData];
format = hasAlpha ? GL_RGBA : GL_RGB;
glTexImage2D( GL_TEXTURE_2D, 0, format, width, height, 0, format,
GL_UNSIGNED_BYTE, bitmapData );
Apart from being unnecessarily imperative, and therefore unnecessarily
verbose, it is very bad code. If the image does not a power-of-2
dimensions, the call to glTexImage2D fails, and nothing is displayed.
My mistake was to trust the world. This code is buried four levels
down in a simple application that's called NSOpenGLFullScreen. The
demo app coder thought this was too boring of a problem so instead of
writing a correct application that displays a single image full
screen, it shows a rotating earth that you can title, pan, and adjust
the lighting on. The original subject matter is lost in a mess of 500
lines of code packaged in four classes. Too bad for me.
My optimism about the simplicity of the problem cost me four days.
I got caught up in believing that "texturing" was the right solution
to drawing an image in OpenGL. I got lost understanding how to
texture a rectangle. The example uses a sphere, which wouldn't work
for my problem, or for most problems. Indeed, there was another
example that shows a movie on a teapot. That's very nice, but it's
not something most people really want to do. Indeed, all I wanted to
do was put a simple image on the screen, right side up.
The nice thing about choosing the Earth is that you have to rotate it
every which way:
glRotatef( rollAngle, 1.0, 0.0, 0.0 );
glRotatef( -23.45, 0.0, 0.0, 1.0 ); // Earth's axial tilt is 23.45 degrees
from the plane of the ecliptic.
glRotatef( animationPhase * 360.0, 0.0, 1.0, 0.0 );
glRotatef( 90.0, 1.0, 0.0, 0.0 );
Cohesion is the name of the game in programming. The more cohesive
your code, the better it is. 23.45 is not cohesive. Indeed, it's
stupid. On a 1280x1024 screen, it's .45 is invisible to the naked
eye. Moreover, this mess of code confuses the reader, because it
turns out that with OpenGL images are rendered upside-down if you
don't flip them. No amount of rotation will get it right. However,
I'm not a graphics guru, and I thought somehow this magical rotation
also corrected the flipping problem. The demo coder failed to mention
this minor fact.
Coupling should be loose. You should be able to call a routine
setFlipped, and it should flip something. The NSImage class has such
a method, and even an isFlipped method. How orthogonal! How wrong of
me to assume it actually does anything.
If you set flag to YES and then lock focus and draw into the
image, the content you draw is cached in the inverted (flipped)
orientation. Changing the value for flag does not affect the
orientation of the cached image.
Even after calling "recache". The image isn't flipped. Andrew
Platzer writes:
[NSImage setFlipped:] has inconsistent behaviour depending on
whether it's cached or not. Unfortunately, it can't be changed
since the existing apps depend on it's behaviour. If you need to
draw an image in a flipped coordinate system, instead, use
NSAffineTransform to undo the flipping and avoid -[NSImage
setFlipped:].
Here's the code Andrew recommends:
NSGraphicsContext saveGraphicsState]
NSAffineTransform *transform = [NSAffineTransform transform]
[transform translateXBy:point.x yBy:point.y + [image size].height];
[transform scaleXBy:1.0 yBy:-1.0];
[transform set];
... draw image @ (0,0)...
[NSGraphicsContext restoreGraphicsState]
Unfortunately, there are some floating point numbers in the above, and
OpenGL does not guarantee that your image won't be mucked with other
than flipping. I'll repeat my solution:
while (--h > 0) {
bcopy(&data[h * bytesPerRow], &flipped[(height - h) * bytesPerRow],
bytesPerRow);
}
It's likely that this solution will work on any image, and it can be
written to the full screen. Remember that's my problem, and
NSGraphicsContext can't be full screen. In order to use Andrew's code
on the full screen you have to use a magical class CGGLContext, which
gives you a CGContext, which is not an NSGraphicsContext, and
moreover, the CGGLContext is not documented at all and there are no
examples.
Tests. Probably the hardest thing to do when writing code is getting
it right. That's why XPers document their assumptions in tests. The
NSOpenGLFullScreen code does not come with tests. Nor do any of the
Apple demo apps. That's why they are all wrong. Here's one of the
biggest errors:
// Set memory alignment parameters for unpacking the bitmap.
glPixelStorei( GL_UNPACK_ALIGNMENT, 1 );
Well, this is wrong. The magic number is 4, not 1. If you have an
image that has a power-of-two dimensions, you could put any unpack
alignment in there, and it will be fine. Alignmments are powers-of-2,
because they are about low-level computer bits that most programmers
shouldn't have to deal with. If the image is a power-of-two, it has
no alignment problems. It's already correctly aligned, always, no
matter what any of the other parameters are. (Really, the image must
be at least 2^2, but that's a pretty small image to be rendering.)
All Apple examples I could find, have GL_UNPACK_ALIGNMENT as 1, and
they are wrong. The demo images they use are all power-of-2
dimensions, and the worst part is they don't mention this assumption.
Most old-time programmers such as myself joke about SMOPs. I was
caught by this one. I thought it was just a SMOP. It wasn't. I went
down a zillion wrong paths. I didn't focus on my first problem: the
images weren't showing up. Yes, I'm pissed at the Apple examples, but
that's just bad code, and most code out there is awful. It's great to
be optimistic, but I forgot that optimism is the biggest assumption of
all.
Moreover, I started with the assumption that it was good to start with
demo programs, and pretty much stuck with that assumption until I
learned that glDrawPixels is what I needed. There are no glDrawPixels
examples, which means that I had to throw out the examples, and start
a freshly written app (which led me down a serious diversion with the
Xcode IDE, but that's another subject entirely). Once I did this,
pixesl started showing up on the screen, because glDrawPixels always
draws pixels even if the image doesn't have a power-of-2 dimensions.
Once I saw the 1950s TV screen, I knew I had a synchronization
problem, and I was able to do a binary search on the parameter space
to get the image aligned right.
My program is done, and I'm still beat so I'm taking the rest of the
day off. That was another mistake I made. I was staying up to all
hours of the night and morning trying to solve this problem. If I had
walked away, like I'm doing now, I probably would have solved it
faster.
What I learned from all this is that XP has some pretty good
practices. I wish I had followed them from the beginning...
Rob