not a beautiful or unique snowflake (nothings) wrote,
not a beautiful or unique snowflake

one-year-of-programming report

I've been working at RAD Game Tools for just over a year now, so I decided to take stock of what I've produced on the programming front.

One thing I realized is that this is probably the first time (at least since 1993) that I've worked steadily (i.e. ~40-hour work-weeks) on a project that I had total control over and could do in my own style, without overhead of communicating with other people. (I've had that sort of isolated control over my game development projects, but I never worked as consistently, with steady hours and without side-tracking onto other projects.)

As everyone knows, "Lines of Code" is a horrible metric except there aren't any better. LoC is actually serviceable if you're not gaming it, but only for comparing someone to him- or herself. Comparing cross-programmers is meaningless, and especially cross-tasks or cross-disciplines. (Operating systems are harder to author than compilers, which are harder to author than "normal" applications, which are harder to author than scripts.) But, there's still nothing better, and I wasn't gaming LoC.

So, in my first year at RAD, here's approximately what I produced, between my project and some shared libraries. I was not gaming LoC; if anything, I tend to squeeze things more vertically so I can fit more on the screen at once (especially since I use larger-than-average fonts for programming), and library size is one of many constraints I optimize for, so code avoids redundancy as much as is reasonable.

55,286 lines of C/C++ files (48,423 non-blank; 43,940 neither blank nor only-a-comment)

66,312 lines including .H files (but there's no actual code in the .H files, unlike in some C++ projects)

Most of this code is for a videogame user-interface library which primarily consists of a swf file player (i.e. for Flash content). This includes an Actionscript 3 virtual machine. Overall I'd say it falls in a similar area of complexity as a compiler. Additionally, all the design/programming operates with a constraint of minimizing run-time performance and run-time memory usage (that is, I'm always thinking about those things as I code).

Sometimes this went faster than my normal programming because I could just read the specifications Adobe has published and implement those (this is how I did JPEG in a day and Ogg Vorbis in a week). Sometimes it went much slower because I had to spend a lot of time reverse-engineering the actual behaviors that Adobe didn't bother documenting. So that's at best a wash how it affected my productivity (at worst it made it slower overall).

Here are 100 lines of non-blank source code chosen at random (some have been split to avoid messing up browsers):

                        backpatch b;
   // if no curves, and we're not doing analytic antialiasing, then the cache is scale-free
         swf_edittext *t = (swf_edittext *) movie->chr;
   int m = r->filter_mode;
      else if (c->type == DISPLAYABLE_button) {
   if (target == 0)
   vert[3][1] = py1;
            set_qslot_raw(z, old[i].q, old[i].slot);
   U16 firstcode[16];
static GDrawTexture *RADLINK gdraw_MakeTexture_Begin(GDraw *g, void *owner\
  S32 width, S32 height, U32 flags, GDraw_MakeTexture_ProcessingInfo *p)
   if (a->was_allocated)
      linepart prev, next;
                  RECT srect;
//	that means we only get 1<<(32-BLOCKINFO_SHIFT) of them
      // tried to free something we hadn't allocated!
as3value make_uint_GC(as3vm *vm, U32 x)
                  return error(VERIFY_FAIL);
      int w,h;
   while (*text) {
         return error(GSwf_Error_Verification_Failed);
         g->m = o->matrix;
            slot = traits[i].slot_id-1;
   STBUG_FIELD        (asFunctionClosure, as3value, proto)
         //     1. always draw as exactly one pixel wide
      case BUTTON_STATE_idle:
   return p;
         } else
      stroke_point ca = { v[a].x + t*da.x, v[a].y + t*da.y };
         if (!vm->gc.need_grey_sweep)
   rrAssert(menu_depth > 0);
         //     A. always draw as a single color
            if (!s) GOTO(typecheck);
            case MC_argcount:
   static F32 scroll;
static rrbool set_qm_loc_GC(as3vm *vm, asObject *o, as3value *v, as3value r, rrbool init)
         case DISPLAYLIST_framelabel: break;
      case NATIVE_date:
         *value += > gx+gw/2 ? pstep : -pstep,
   // check if we're open, but have a pending close operation...
   // for floating point depth, just use mantissa, e.g. 16-20 bits
            stb_fatal("Couldn't parse '%s'", str);
#include "rrstacktrace.h"
               U32 num,k;
      default: rrAssert(0); break;
   void *p = rrSystemMalloc(bytes, actual_bytes);
//         |  oooooooo oooooooo |   o = offset from system malloc to ptr (varies with alignment)
   { ISTR_normal,            L"normal"   },
      static S32 bleh;
            case 'Q': {
// we check if it's going to overflow "soon" and force a collection.)
         S32 i;
static void accounting_alloc_block(U32 size, U32 id)
            if (GC_color(e->gc) == GC_white)
   // this will log which will call malloc which is evil :
//    buffer:   the output buffer, where the utf8 string is created; if NULL, \
       no output is created, and the return value is the length needed (or RR_UTF8_INVALID)
         swf_matrix_concatenate(&xf, pxf, &c->m);
   // link interfaces
   cpool->string          = abc_cpool_string(stream, cpool->string_count);
static U32 RADLINK stbalignment(void *handle)
   a->output_remain = len;
   { op(inclocal), { MC_reg_rw } },
         if (s->y0 < 0) s->y0  = 0;
void swf_matrix_concatenate(gswf_matrix *out, gswf_matrix *a, gswf_matrix *b)
      // we maintain a cleared _bounding box_ of what people have
   h += h >> 15;
   as3value_pool *p;
#include "swf_data.h"
                  case MC_argcount2:  // read a U30: then pop 2x many\
                                       values (to use as function arguments)
            c = add_vertex(d, &mid, CENTER(d));
   c->super = findclass(p, super_name);

I sampled 1000 lines and chose the ones that pack the most complicated logic on one line:
                        u30(asm_uint(a, strtoul(tok[k++]+2, NULL, 16)));
   if (get_ub(s,1)) t->flags |= EDITTEXT_WordWrap;
         rrAssert(!string_eq(tab[p].str, v->str->string.string.stringlen, v->str->string.string.text));
            words = WORDCOUNT(sizeof(as3vm_array_data)) - 1 + ((as3vm_array_data *) e)->array_limit;
      d->one_pixel_width = (0.75f - 0.25f) / (d->center_dist - 0.25f) * d->width;
               stack1 = make_int_GC(vm, v2i(stack1) << (v2i(stack0) & 31));
               if (!ToNumberDirect_GC(vm, stack0, &v2)) GOTO(typecheck);

That sequence of "v->str->string.string.stringlen" isn't actually even the worst that it ever gets, sadly.

The last line represents a very common pattern in my source code--the error handling is often compacted onto one line with a goto/return at the end, because it doesn't affect regular flow control (it's effectively an exception), so I want it out of the way of visually parsing the "normal" path.

An interesting thing I didn't realize until just now is that if show me a complicated-enough line of code from this project, I can tell you what it does (because it's complicated, it gives itself enough context). Obviously I know the universal macros in this project and I know my own naming conventions, so I'm not sure if this is actually surprising:

                        u30(asm_uint(a, strtoul(tok[k++]+2, NULL, 16)));
The above line must be part of an AVM2 (the actionscript 3 virtual machine) assembler which I wrote to help debugging (I made a suite of tests for each opcode in the VM)--it won't ship. u30() probably outputs an at-most-30-bit-integer in a variable width format designed by the spec. I'm not sure why there's both u30() and asm_uint() now.

   if (get_ub(s,1)) t->flags |= EDITTEXT_WordWrap;
This is part of the SWF-file parser, parsing an 'edit box' object. get_ub() gets N unsigned bits (here N=1) from the bit-packed SWF stream.

         rrAssert(!string_eq(tab[p].str, v->str->string.string.stringlen, v->str->string.string.text));
This assert is checking to make sure the contents of v, an actionscript string pointer, isn't the same as entry #p in some table. I'm can't think what table this would be. It's probably while loading/initializing the file, possibly making sure some hash table is unique or that the same class name doesn't appear more than once.

            words = WORDCOUNT(sizeof(as3vm_array_data)) - 1 + ((as3vm_array_data *) e)->array_limit;
This is computing the physical size of an Actionscript array-data object (the contents of an Array). 'array_limit' is how much space is available, not how much is used. One place you might do this is if you wanted to allocate an array-data object, but the cases where you would do that (cloning an array or internally resizing it) you'd use the minimum amount needed, i.e. the amount that's in use, not the amount that was allocated. So this must be inside the garbage collector. The GC *also* should only process the part that's used, not the full allocation, but it needs to know about the full allocation size if it's moving it from the young heap to the old heap (it's a copy collector), so that's my guess.

      d->one_pixel_width = (0.75f - 0.25f) / (d->center_dist - 0.25f) * d->width;
This is part of the wide-line rendering system; I believe it is computing what fraction of the line's width is one-pixel-wide on screen. It's doing that by making calculations from the texture coordinates used for antialiasing wide lines ("d->center_dist"), since those have to have been computed to properly account for screen-space (since the anti-aliasing wants to happen within a single pixel's width).

               stack1 = make_int_GC(vm, v2i(stack1) << (v2i(stack0) & 31));
This is obviously in the AVM2 interpreter inner loop, implementing integer left-shit--the fast path where both inputs are already unboxed integers. v2i() untags an integer. make_int_GC() may return a boxed integer; creating it will require allocating memory, which might trigger the garbage collector, hence the _GC() suffix. (I have to always know which functions might trigger a garbage collection to make sure all temporary variables are visible to the collector during that call, since the collector can relocate them.)

               if (!ToNumberDirect_GC(vm, stack0, &v2)) GOTO(typecheck);
The 'GOTO(typecheck)' means this is also in the AVM2 interpreter inner loop. It's checking that the argument on top of the stack--which "v2" means is the "second" value to the conceptual operation--can be converted to a number. (This conversion can run arbitrary actionscript code, so it might trigger the garbage collector.) I don't know which opcode that would be--probably another arithmetic one.

The above examples overrepresent the Actionscript processing over the graphics stuff, but this is probably accurate since I was filtering on 'complexity'; the graphics code is much more likely to be straightforward. (The Actionscript interpreter is under 15KLoC, i.e. around 1/3 of the project.)

As I said, comparing LoC is potentially meaningless. On the other hand, if I have a more-compact-than-normal style and I'm working on harder-class problems than someone else, my LoC versus theirs should underestimate the difference my productivity relative to theirs. (On the other hand, my code is not yet 100% debugged.) So, whatever, I will go ahead and point the following "meaningless" numbers out, found by googling.

Googling also revealed people claiming to code anywhere from 20,000 to 60,000 LoC per year.

I'm not implying that I could create this much code if I were working on a project the size of Vista (productivity is known to decrease with increasing project size). And I'm not saying I could create this much code if I were working on a team (productivity is known to decrease with increasing team size; see e.g. The Mythical Man Month).

In fact, this is part of why I can't see myself ever going back to working on actual videogames on a team with more than, oh, 3 programmers. I have no idea what the actual number would be, but I feel like it would be something more like 5-20K LoC. And I'd hate it. (I wouldn't hate the fact that the numbers were lower; I'd hate the experience, all the overhead and drag and whatever that is implied by that drop in number; the day-to-day experience that is implied by the drop in that number.)
  • Post a new comment


    default userpic

    Your reply will be screened

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.