Clunkdraw

Clunkdraw is a small, clunky clone of Apple's original monochrome Quickdraw imaging library. (Written from scratch in the 1980's based on the published APIs.)

Clunkdraw Windowing Screenshot    Clunkdraw Clipping Screenshot
Clunkdraw Large Text Screenshot    Clunkdraw CopyBits Screenshot
First implemented here (in 2021) as a Free Pascal library targeted at driving Waveshare's small e-Paper displays for Raspberry Pi. The above sample displays (from the included test program) demonstrate that you can utilize multiple overlapping display windows (if you want them), clipping to arbitrary shapes (ditto), multiple font styles, sizes, and rotation, full justification of text, large font sizes, a variety of geometric shapes, and bitmap scaling and orientation.

Clunkdraw in Pascal tarball: clunkdraw.tgz


Now in C! Recently I needed it in a C project and did a quick-and-dirty port of just the pieces I needed. That worked, but I felt that I'd done it a disservice so I spent a little extra time to do it thoroughly. Besides supporting the same e-Paper displays the Pascal does it can render 2-, 4-, and 8-bit grayscale, as well as 16-bit RGB565 color. (It is still essentially a colorized monochrome package intended for business graphics.)

Clunkdraw in C tarball: clunkdrawc.tgz

A demonstration of Conway's Game of Life written using Clunkdraw on a Raspberry Pi 3B+ driving a 320×240 Adafruit 2.2" TFT LCD 16-bit color display:

Acorn Movie
Just how much faster does something like Clunkdraw need to be? (The source for this 12kB Life program, which can optionally (with the help of ffmpeg) make the movie, is included in the package.)
Ah, Life. This one, running at around 140 generations/second on a 320×240 field, is greatly superior to the first computerized Life I ever ran, 45 years ago on my old 1802 Elf with 1kB of RAM: about 2.5 seconds/generation on a 64×32 field! Very sluggish1, it would have taken nearly four hours to run an acorn to ground, if an acorn could have survived to term on the smaller field. (It could not.) As quick as this is, compared to that old Elf, it's at least that far behind the current state of the art on home computers.

Background

Once upon a time there was an intrepid Small Company making banking computers. Their first products were purchased North Star S-100 systems, but they soon started making their own S-100 cards, and later their own machines, both bus-based and not. All were based on the wildly popular 8-bit Z-80 microprocessor, often multiple CPUs were used. They made multiple-user machines, they had communications coprocessors for talking amongst themselves and with Big Iron; video subsystems existed that supported Arabic and Hangul (Korean) displays, in addition to European languages, camera capture, graphics display, etc. They ran CP/M and MP/M, in addition to the Company's proprietary multi-user multi-tasking runtime OS and single-user single-tasking development OS. The Company was successful, but the necessity of using assembly language for all software (for both performance and space reasons) was a heavy burden, and was fast becoming crippling. (The 'hard' 64KB address ceiling was probably the single most limiting factor.)

The 1984 introduction of the 68000-based Macintosh electrified the entire microcomputer industry, this company included. Proof that a modestly priced 16-bit system using a high level language (like C) was not only at least as powerful as the Z-80 and supported a much larger flat address space, dramatically easing programming, but was capable of supporting nearly any customer's written language using a graphics-only hardware platform, and was even capable of supporting a paradigm shift to a GUI environment.

(Apple's high list prices did not disuade the engineers at the Small Company, they were well aware of the very modest incremental component cost over a Z-80 solution. While Intel's extant 8086 CPU family also would support more memory than a Z-80, the Company was already highly experienced with the memory bank switching and code segmentation necessary to get around the Z80's 64KB limit, and had absolutely zero interest in jumping 'forward' to a 'solution' that still required all the same kind of software machinations in order to avoid the very same 64KB limit. Not Good Enough, and thank you for playing, Intel. Other reasonable candidates were offered by Zilog [Z8000, still with a marked 64KB odor], National Semiconductor [16032, late, slow and buggy, though elegant], and Western Electric [WE32100, obscure, but also elegant], but Motorola's offering was the most mature, and support software [OS, C compiler] was readily available, as were second sources of parts through Hitachi. Intel's own 386, their first product to actually compete effectively with the 68000, was just becoming a potential choice at this time, but it was new, expensive, and deliberately single-sourced, all of which removed it from consideration. Also, its primary goal was compatibility [with a processor we weren't already using] rather than capability, and so it was much less interesting to the Company than other choices.)

The subsequent releases of comparable low-cost Atari and Amiga systems hammered home the proof, and commercial releases of Unix-based systems using the same 68000 processor family showed just how capable these systems could be. All of a sudden the Company had a clear direction in which to go, one that would be a significant improvement over the Z-80 product line, both in customer perception and in ease of development.

However, the first 68010-based next-generation systems the Company developed suffered from a lack of vision on the part of management. While the hardware was very good, as was the DNIX-based system software and the DIAB C compiler, the application layer software under consideration at the time was very pedestrian, and was based around a VT220 non-graphics 24×80 terminal model that was barely even capable of supporting European languages. (It didn't, but could have easily enough.) No Arabic, no Hangul, and no graphics.

A major problem was that while everybody involved could see the improved capability of the new product line's hardware base, they had essentially all spent that same performance dollar, resulting in severe under-performance in the final result. Crippling underperformance, in fact. It had been forgotten that the 68010 'advantage' had already been spent: on using C instead of assembly language. The 68010 had only twice the clock rate of the Z-80, and twice the bus width. Yes, it supported a lot more memory directly and thus eliminated bank-switching considerations, and virtual memory meant that the memory ceiling was very 'soft', and the CPU's more and larger registers cut down on all the unproductive data shuffling that generally plagued 8-bit processors, but testing had shown that when using C it wasn't really all that much faster than the Z-80 had been at running custom application code. And, the graphics-only video hardware of the new platform was decidedly more sluggish than the more expensive (and memory-thrifty, but far less flexible) dedicated-language video hardware of the Z-80 platforms, so even if there had been a performance surplus it was more than gone already. (The new architecture was expressly designed to improve development time, not application speed—application execution speed was basically never a problem on the old products.) The design intent here was simple: write efficient applications the same way you did before, but 1) you get to use C instead of assembler, and 2) you don't have to waste a lot of time packing your code down to fit in a Procrustean 64kB memory bed.

(We're getting there, be patient!)

Disaster Strikes

The application team's management decided to spend 'their' share of the platform upgrade bounty (all of it, naturally, rather than their true share: none of it beyond getting to use C) on moving to a fourth-generation database language product instead of using any form of traditional procedural code, and upon that rock the Company's ship foundered.

The prototypes were slow. Orders of magnitude (yes, more than one) too slow. It was common during development to push some menu keys and then go to lunch. Once you came back it might be ready to look at to see if it had done the right thing, but lunchtime wasn't always long enough. The best engineers in the company were thrown at the problem, and told to 'optimize' it. They tried, and made some headway, improving its speed by 4×, but the starting point was so bad... The selected architecture was simply not practical on the platforms of the day.

The language vendor, when contacted, said "Don't do that, that language is meant only for rapid prototyping of database queries. [This was before the widespread availability of SQL.] Why aren't you accessing the database using the conventional API?" High-end engineering workstations, many times more expensive than any customer would consider purchasing for their bank tellers, underperformed by at least one order of magnitude when running the new code, even when optimized. The Company's own engineers unanimously said "this is bad and can't be fixed, we have to do something else." This all fell upon deaf ears.

Management hired performance consultants to show the Company's engineers the errors of their ways. The consultants tested the new platform, its hardware and system software, and ranked it higher than anything else in its class they had ever seen. Their report basically said: "Listen to your people, they're very good." No matter, management had somehow hitched its wagon to this particular 'solution', and that was that. The deaf ears became hostile ears: The engineers were told that anybody who persisted in nay-saying had better just shut up and fix the problem, or else seek employment elsewhere.

Of course, it was not just that management had chosen the wrong software architecture for the new product, if you don't make an occasional mistake you're not trying hard enough. The sin was that they were completely unwilling to admit that it had been a mistake, and to do something different while it was still possible to do so. The engineers could not solve the architectural problem, and the managers (many of them from a mainframe computing background and still vaguely resentful of the limitations of the microcomputers they found themselves stuck with) would not budge on the architecture. The Company's entire future was tied to this one decision; the managers were betting the farm, not that it was their farm to bet. And the months, eventually years, rolled by...

Recovery

Eventually uppermost management, the founder of the Company and chairman of the board, heard about the stalemate via a chance men's-room conversation. It hit the fan, so to speak, because he had heard nothing about the critical performance problem until then, and had been led to believe that the new product was almost ready to deliver—a complete and utter lie. (A large amount of new code had been written, under duress, with the assumption that someday it would magically begin performing acceptably.) A tiger team was immediately formed to come up with an alternative, and it knew that it had to hit a home run first time at bat, and quickly. The alternative needed to be a safe bet architecture- and performance-wise, and use the new hardware as-is, but it also needed to be showy, and ready soon. There was no time to redesign anything but the application. We were late, the Company had had nothing new to show for more than two years, as the entire engineering staff of the company had been dedicated to trying to turn the festering pile of shit into a gourmet meal, and the customer base and industry pundits were getting concerned. (They had, naturally enough, heard rumors of upcoming new products, but usually by that time they'd have gotten some sneak peeks at prototypes. This stuff was so bad that nobody would show it to anybody for any reason whatsoever; not even the most gung-ho salesman would risk letting a customer see it in operation lest they be laughed out of the building and lose all future credibility.)

To recap: the new hardware and system software was best-in-class, and the system had enough memory and performance to rapidly deploy new procedural applications written in C. But all that was available to the application for user I/O was a lackluster VT-220 subset terminal emulation. This was no better than what the old Z-80 systems could offer, and in fact was worse because it was really quite sluggish in comparison. (A Z-80 could drive, using assembly language, a memory-mapped hardware ASCII video subsystem extremely fast. Video responsiveness of all the old systems was unsurpassed in the industry.) The new video hardware had met its design targets of supporting the written language of any customer, and supporting facsimile (signatures, checks, etc.) graphics, all at a modest cost, but there was no system or application software to support this, only the terminal emulator, and the innate performance of the display hardware was decidedly marginal as it required system software to do all the character generation rather than using custom hardware dedicated to that purpose.

We needed something that looked better, a lot better, to draw attention away from the fact that applications on the new platform felt noticeably slower than on the old. We needed something that could better support languages, and facsimile graphics, than the terminal emulation or the old products. Emphasis on what you've gained compared to the old, not on what you've lost. Something dazzling enough to help explain away the delay in providing the new product, since we weren't about to make public the real reason we were late.

The X Window System existed by then, but all known implementations themselves consumed memory in excess of the total available on the target system, leaving none for the application or the operating system, and only had been seen working on much faster hardware than the target system, so it was of no real interest or use.

(And we're here, finally.)

Enter Clunkdraw

Because we knew that a Macintosh, on a similar modest hardware base, was capable of doing the required tasks satisfactorily and very attractively, we knew that Quickdraw, the Mac's Pascal/assembler lean-and-mean imaging engine, was thus capable. So we knew that its imaging model, and API set, was sufficiently capable provided the implementation was well done. The 'safe' choice for us was to follow their API model using a newly written subset library that directly accessed the video hardware, refining the library as necessary until it had the features we needed and the performance was satisfactory. The first Clunkdraw, written in C, very much lived up to its name, and supported only MoveTo/DrawText using Quickdraw's coordinate system and one fixed-pitch font (taken from the terminal emulator), and EraseRect. But it used the expandable API model of Quickdraw right from the start, which meant that new features could be added as we needed them without having to waste precious time rewriting existing application code—unless we chose to. Almost immediately we started dressing up the new application: getting off the 24×80 character grid, using lines and boxes, proportional fonts in multiple sizes and styles... As Clunkdraw and its one client evolved more features were added, and all the low-level drawing routines were written (or rewritten) in assembler for performance. (This process was guided by profiling of the actual code, application and all, rather than by guesswork. The drawing library was always the bottleneck, so that's where the focus remained.) This was very painful and slow to write, as assembler always is, but the performance edge thus gained was sufficient to stay generally below the 'too-slow' perceptual threshold, and the entire rest of the application never needed to use assembly language. (Only one member of the development team, your author, ever had to labor along using the new product's assembly language. Everybody else could churn away happily in C.) The application was rapidly getting much prettier, while not slowing down in operation, and we held to the Quickdraw API model because we knew that it was good enough for the results we wanted. At some point Clunkdraw really wasn't that clunky anymore, but the name had stuck.

The alternative product was demonstrable (to customers) within three months, and was a roaring success. (Did we mention that the engineering staff really was quite good?) Some heads did end up rolling regarding the database debacle, as one might expect. Unfortunately the Empress database itself also got caught up in the purge, although it was not at fault and was itself a good performer. We had paid a million dollars for it and its M-Builder query prototyping language that we'd mis-applied as an application environment, and we ended up throwing it all away. We eventually wrote our own application-specific database, but we'd probably have been better served by using the one we'd already bought. But, feelings were running pretty high at that point...

Eventually Clunkdraw gained a network-independent client/server layer, vaguely similar to what X Windows offers, which dramatically improved what could be done with the system, especially in training, supervisory, and remote debugging roles. But the imaging model remained that of Quickdraw. We also ported Clunkdraw, in C and assembler, and the top-notch DIAB C compiler itself, to the TI 34010 graphics CPU for use in our later color products. At some point gateway programs to both Windows 3.1 and Macintosh systems were created, allowing them access to our financial applications. (The on-the-fly translation from Clunkdraw to true Quickdraw was particularly easy, as you can imagine, but the network connectivity was much more difficult, as we did not use TCP/IP but rather an Ethernet form of X.25 networking, and the target Macs didn't even use Ethernet.) Eventually there were many man-years invested in this code base, and it was generally highly satisfactory within its market niche.

Final Result

The Company's old Z-80 products were reasonably priced, extremely performant, and extraordinarily difficult to program as well as having a dated appearance in operation. The new 68010 products were also reasonably priced, sufficiently performant (thanks to Clunkdraw), and easy to program (thanks to sufficient RAM, virtual memory, and C), and had a much more modern (also thanks to Clunkdraw) look. Success! The tiger team received some significant bonuses that year, as well as the benefits of continued employment. (Sadly, the delay in new product ultimately did cause the Company to be sold and merged with a competitor, curtailing the life of the new products and ultimately the jobs of most of the employees.)

Modern (2021) Times

The Clunkdraw products discussed above came and went in their time, and eventually the Company was no more and there were no Clunkdraw-using products left in the field. Clunkdraw itself languished for decades on a disk that I had kept for nostalgia's sake. The bulk of it was 68010 (or 34010) assembly language, there was little chance that I would ever need to even look at it again...

I eventually found myself again working for the same man that had founded the Small Company. I was looking at using a small monochrome e-Paper display, because of its unequalled strengths in the ambient lighting department. Unfortunately the demo library that came with the display kind of sucked, especially the text, and I found myself waxing nostalgic for the simplicity, capability, and familiarity of Clunkdraw. (Especially as regards good-looking large-scale text.) The e-Paper display did not integrate into the host Raspberry Pi's GUI system, you are intended to drive it as a pure peripheral, using an I/O library of your own. (Its use model resembles printing on paper: think flash memory rather than RAM, so the standard display channels aren't a good fit for the hardware.)

And then the light went on: Why not resurrect Clunkdraw? At least partially? The environment I needed was Pascal, but Quickdraw itself was clearly very happy in Pascal, since it started out there, and porting from basic C to basic Pascal is a near-trivial mechanical exercise. The RPi is very fast, avoiding assembly language probably isn't a hardship now, and the target display was not large. And I only needed a couple of calls and one or two of the nicer-looking fonts... all the familiar old arguments. And so the die was thrown. All I needed to do was a mechanical translation of C to Pascal for the few parts I needed, and to write some naive pixel-plotting code to replace the elaborate optimized assembly language of original Clunkdraw, and I'm done. It looked like only a few days would be required for what I needed in the new product, and the results would be infinitely better-looking and more familiar than what I'd get continuing on with the e-Paper demo library...

It was so much fun and worked so well that I got carried away, and ended up porting everything in the monochrome imaging layer over the course of a month. (Not the networking layer nor the color code, which were themselves substantial and not a good fit to the new environment.) The new drawing code itself is very naive, and an extremely poor performer when compared to original Clunkdraw. But, the RPi is so fast and our needs were so modest that it simply doesn't matter here.

No, it's not C, and it's certainly not C++. It's a near-trivial exercise to port it to C, though. It is probably a nearly trivial exercise to port it to any procedural language environment.

Partitioning

Original Clunkdraw was built as a fine-grained .a library (remember this predated the advent of shared object libraries, and certainly predated Linux) so that a client application needn't contain any code it wasn't using. Pascal does not really support this model. Rather, it prefers larger-grained units. Clunkdraw in Pascal has been split into nine units, mostly just to make it easier to work on.

Also, original Clunkdraw stored all its fonts on disk, in part because the sum of all its fonts occupied a substantial portion of a client's address space, but this definitely introduced an unwelcome external dependency. The RPi supports much larger address spaces, so this implementation puts all but one of the fonts in an optional unit, a large one, but one which can be avoided if you don't need the fonts. Clunkdraw in Pascal relies on nothing outside of itself, and adds no additional dependencies at either compile or run time.

Because it doesn't look like Pascal supports the same concept as C's function pointers, which allows for late binding, the number of units is relatively small, and things are packaged together. Unit X refers to Y, which in turn refers to Z, and you end up including a bunch of the units as often as not. This could maybe be done better by an expert, which I am not. But, this is good enough for now.

There are nine units, plus test programs and etc.:

Source
Size
Object
Size
FileContains
105,568 16,001  epaper.pas E-Paper SPI access
155,901 58,776  clunkdraw.pas Lines; Rectangles; Region clipping, drawing; Support
5,342,618 1,087,081  clunkfonts.pas All but one font
30,367 8,724  clunkpoly.pas Polygons
38,358 11,940  clunkregions.pas Region manipulation
33,800 7,994  clunkrounds.pas Ovals; Arcs and Wedges; Rounded Rectangles
6,805 2,305  clunksave.pas MacPaint and PBM file saving (for screen shots)
70,359 25,802  clunktext.pas Text drawing; Default font
91,510 21,396  clunkwinds.pas Overlapping windows
The size of this library is no problem at all for a Raspberry Pi, or something similar, but it's far too big for use on an Arduino. A fine-grained .a C library of Clunkdraw might be useful on an Arduino, if you were not using very many of the Clunkdraw features. I have considered porting Clunkdraw again, back to C for this purpose, but so far have not done so...

New for 2023

... until now!

I needed to do some drawing on an overlay (over a video feed) on a small RPi RGB565 screen, and my thoughts turned again to Clunkdraw. Its drawing model was never strictly monochrome, and given the naive low-level everything's-a-pixel implementation it wasn't even difficult to extend it to color. But, I needed it in C this time. I first did a quick port of what I needed from the Pascal files, but this did not please me. Clunkdraw deserved better, and what of the poor memory-limited Arduinae that might wish to avail themselves of it...

Well, in C I do know how to chop things up finely. For an embedded, dedicated solution a shared library offers no advantages whatsoever, and has the significant disadvantage of always having all the code resident, whether you need it or not. It also requires at least a rudimentary operating system on the target device, which isn't usually a feature of small embedded systems. But as a .a library, non-shared, any given client, on any kind of platform, can limit itself to only what it needs from the library. (Case in point: a "Hello, world!" application weighs in at just under 10kB, font included.)

I grabbed the Pascal sources and had at it, putting each function into its own source file as I converted. There are currently 614 source code files, including fonts. This translates into 614 members in the clunkdraw.a archive.

Let's Get Small

Clunkdraw in C relies on nothing outside of itself, and adds no additional dependencies at either compile or run time. Clunkdraw does not use floating point, and uses only integer math. Clunkdraw uses some crumbs from libc, malloc in particular for gaining working storage, and alloca. (It frees things when it is done with them.) So, the very smallest devices, those that can't support a backing store, heap memory, and a good-sized stack are probably excluded. Lesser devices, likewise, probably can't tolerate the naive implementation as they would be unusably slow. The SAMD class devices seem like they ought to be a practical minimum, as might RPi Pico.

If you include the library in your final compile, the linker will extract only what you need from the library.

cc testhello.c clunkdraw.a
Of course, without some degree of care you'll still get most all of it, especially the fonts. That's because Clunkdraw, by itself, doesn't know which of the available fonts or features you're not using. The parent QuickDraw drawing model doesn't require you to declare up front what features you're going to be using, and so in a sense it has to be prepared for anything. That tends to bring in all the code, hampering us when we're trying to be small.

C's function pointers, which allow for late binding, allows some degree of run-time insulation in the inter-module references. This means that, with care, you can avoid including large chunks of Clunkdraw that you don't want.

If you don't want the font scaler, don't call InitFonts(). If you don't want the windowing system, don't call InitWindows() or any of the other windowing subroutines. In fact, if you draw anything other than rectangles you might be surprised at how much of the library you'll get. But, all is not lost.

If size matters, you're going to want to get familiar with the link map your compiler can make. For gcc, you get the map via:

gcc ... -Xlinker -Map=file.map
Examine the .map file to see what gcc has taken from the library. The results may surprise you.

There are three general techniques for reducing library code inclusion in these circumstances:

  1. Don't call library routines that you don't really need;
  2. Supply dummy resources for things that will not actually be needed, but which are automatically included;
  3. Supply reduced-capability resources for things that, while needed, do more than you need.
All three are exemplified in the following suggestions:

If you stick to basic text and rectangles you'll be well-poised to keep the library footprint small. For a small embedded-device control panel you might not need anything more.

Return to Site Home