Mac OS X Graphic Performance Tidbits

Quartz Debug

Lately I’ve been digging into the details of the Mac OS X graphics subsystem, both because a lot of my (Java) code exercises it, and partly because I might be building something similar at some point.

OS X owes its renowned visual stability to the fact that every single window is buffered; to be exact, drawing commands from programs are executed into offscreen buffers, which are flushed to the screen (with an eye to where the CRT beam is!) when WaitNextEvent or QDFlushPortBuffer are called. Keeping a bitmap for each window is what allows OS X to sling windows around without visual glitching, even when applications are frozen up compeletely. It is, of course, a very expensive proposition in terms of the system RAM that it consumes. It hadn’t sunk in for me before that every single open window, visible or not, eats up significant RAM—maybe that’ll convince me to close old windows a little more often, especially on my poor 640MB iBook.

The other thing that I had heard about before but hadn’t tried before is the Quartz Debug utility, which you’ll find in /Developer/Applications/Performance Tools/Quartz Debug if you have the Developer Tools installed on your machine. If you run that and check off Flash screen updates (yellow) and No delay after flash, you’ll get a nice view into the workings of the graphics system, because any area of the screen buffer that changes will flash yellow. One interesting thing that this makes apparent is an advantage that Safari has over Firefox: when scrolling, Safari updates only the newly-exposed area of the window, making for smoother scrolling than Firefox, which always redraws the whole content pane. Another thing that it made obvious to me is how horribly inefficient some parts of the Geobrowser are in terms of redrawing things which haven’t changed visually—I need to be more conscientious about calling repaint() with an affected area and minding the clip rectangle in paintComponent(). (It actually makes me happy to discover clear-cut avenues for optimization like that.)

I’m still looking for real nuts-and-bolts details on what happens during screen updates. Here’s the mental model that I’ve pieced together so far:

  1. An application performs some drawing operations, which modify an offscreen window buffer #1
  2. The application calls QDFlushPortBuffer with an affected region rectangle, which blocks while…
  3. The affected region of the application window is blitted from buffer #1 to buffer #2 (the Window Server’s buffer for that window)
  4. The affected region of the screen is re-composited by Quartz Compositor (Extreme or otherwise) and blitted to the frame buffer when the CRT beam or LCD refresh point is elsewhere

If you know better and I’m getting some details wrong, don’t hesitate to comment.

  • http://anukul.com/ Anukul

    A couple of weeks ago, Sam Kass (at Viz) started using the Quartz debugger while VNC/RemoteDesktop’ing into a PC to tune the redrawing of CoMotion on Windows. Of course there could be interference from the smartness of the screenscraping algorithms, but it seems to work pretty dandily in finding excessive redraws.

  • http://h_a_s_h.blogspot.com/ HASH

    You got most of it correct except for when things block. Here’s what happens during a screen update:
    1) Application draws into a window backing store buffer using CoreGraphics, QD or Java API calls. The rendering library render the bits directly into the window backing store.
    2) Applications need to tell the window system that the bits have been updated and need to be flushed. They call the appropriate API to perform the flush. The rendering library maintains a list of dirty rectangles of what was touched.
    3) Note that the flush operation is asynchronous. Control returns to the application while the the Quartz Compositor (window server) uploads the affected regions to video memory.
    4) The Quartz Compositor takes the affected update regions and performs the compositing of the update region in an offscreen vram buffer and then synchronizes the update to visible framebuffer using a beam sync operation.
    5) The next time the application decides to draw into the window backing store again, the rendering library has to lock the window backing store bits down. If the previous flush isn’t complete at this stage, this is when the lock of the window backing store will block. That’s another reason why people see a lot of time being spent in the first drawing after a flush. It incorrectly looks like the drawing operation is expensive, but in reality it’s just waiting for the previous flush to complete.

  • http://retrovirus.com Joe

    Thanks for the clarification, HASH—I forgot about the locking part. So I guess both application drawing operations and Quartz Compositor in step 3-4 try to acquire a lock on the application’s window buffer? What happens if the application has a drawing lock when Compositor want to try to composite that area (or does that just not happen)?

  • http://h_a_s_h.blogspot.com/ HASH

    The application will only write into the window backing store, so will take a write lock. This is necessary if the app has multiple drawing threads drawing to the same window. The Quartz Compositor just needs a read lock for the compositing operation. So it can do so even when there is a write lock from the application. This can result in partial updates to the window backing store showing up before the flush is complete. This is only an issue if you have a transparent window on top of your application’s window that is flushing it’s content and the Quartz Compositor needs the content of your application’s window (even though it has not flushed yet) for the composition. So the net result is a partial update being presented to the user. For something like this Carbon, and very likely Cocoa also, provides the ability to disable updates for your window which tells the Quartz Compositor to not update any region of the screen that your window occupies. Given that an ill behaved app could render the display useless, the Quartz Compositor enforces the app to be done disabling the update for upto 1 second, after which it re-enables updates for the window.