heckmeck!

Nerd content and
cringe since 1999

Alexander Grupe
Losso/ATW

To you, from a secret admirer

A Valentine-themed 512 byte “intro” for Lovebyte 2025 where it ranked 4th place in the oldschool 512 byte competition. Well, it’s mainly a procedural image, but it has sound. Choo-choo! :)

Setup

With Lovebyte taking place on Valentine’s day this year, I had the idea to use it as a theme and adapt a well-known bit from the Simpsons.

That Valentine sure was funny. It says “choo-choo-choose me” and there's a picture of a train!

– Ralph Wiggum

I wanted to draw everything with simple shapes and system methods, like I did in Byte Scene Investigation, and maybe with a slightly different approach this time.

  • Compact startup code to obtain the current screen’s rastport for drawing and a pointer to graphics.library
  • Compress the code with Salvador/ZX0 and with Shrinkler, see what’s smaller
  • A main loop that draws a list of graphic primitives
  • Print the “choo-choo-choose you” text
  • If there is any space left, add sound

Ellipses everywhere

With the startup code in place (similar to the code in B.S.I.)…

    move.l  4.w,a6      ; exec base
    move.l  156(a6),a6  ; IVBLIT.IV_DATA = graphics base
    move.l  368(a2),a5  ; G_INTUITION = intuition base
    move.l  56(a5),a5   ; ib_ActiveScreen
    lea     84(a5),a1   ; sc_RastPort
    jsr     -48(a6)     ; ClearScreen(a1:rp)

…I ran some experiments to see what graphics.library functions were a good fit. It was clear that I would need circles for this one!

FunctionDetails
SetAPen Required to change drawing color, and not really avoidable: Patching rp_FgPen in the rastport structure directly doesn’t cut it
AreaEllipse Gets you a filled ellipse, but the setup is complicated: You also need to call InitTmpRas, InitArea, AreaDraw, and handle the data structures for the area buffer and the “TmpRas” structure. On the plus side, you can combine polygons and ellipses in one go, and you can fake your way around InitTmpRas and InitArea by providing pre-filled mini structures pointing to probably-unused memory
DrawEllipse Draws an ellipse, but only the outline. Plus side: Doesn’t need a TmpRas
Text Required to print text. Nasty: Overwrites register a1 containing the rastport address; coordinates need to be set with a Move call. Plus side: Doesn’t require a TmpRas, either
LoadRGB4 Sets a custom palette. Pro: Looks professional and can hide the mouse cursor. Contra: Takes up space for the palette RGB values, needs viewport as an argument

Seeing that DrawEllipse is a rather “friendly” function (it leaves a1 intact and doesn’t need a TmpRas setup), I re-evaluated the setup:

  • What if I could draw everything with ellipses?
  • Filled circles can be drawn as many ellipses while the radius getting smaller and smaller
  • Or only the width getting smaller. In fact, when I’m already drawing all the ellipses in a loop, I could also add explicit delta-x, delta-y and delta-width values: The ellipses could move, shrink and grow while being drawn
  • For more control, it might be a good idea to calculate with a higher precision internally (i. e. extra bits) and shift the x, y, width, height values to the right to get screen pixel coordinates for the DrawEllipse call. This way, the width could shrink by a fraction of a pixel each time, and the circles could move 1/8th pixel to right and 1/4th of a pixel down, etc. Also, we could get more overdrawing this way, avoiding pixel gaps
  • With this method, we can also draw kind-of rectangles (a very flat ellipse moving down) and extruding/thinning cylinders

After some prototyping and a lot of printf debugging, I settled on this data structure for the ellipse parameters:

    ; Internal coordinates have 2 extra bits of precision,
    ; i.e. screen coordinates * 4
    ;
    ; Kickstart 1.x colors:
    ; 0=blue, 1=white, 2=black, 3=orange
    ;
    ; "count" specifies number of repetitions,
    ; applying delta x, delta y, delta w each time

    ;    col x       y       w      h      dx dy dw count
    dc.w  2, 202<<2, 100<<2, 87<<2, 12<<2, 2, 4, 0, 64
    dc.w  1, 240<<2,  90<<2, 55<<2,  5<<2, 2, 4, 0, 20
    dc.w -1 ; end of list

And this is the rendering code; in each step it’s always drawing two ellipses that are 1 horizontal pixel apart (i. e. a “fat” two-pixel wide outline).

    lea     ellipses(pc),a4
    move.w  (a4)+,d0    ; first parameter = color
.drawloop
    jsr     -342(a6)    ; SetAPen(a1:rp,d0:color)
    movem.w (a4)+,d0-d7 ; get the other loop parameters:
                        ; d0 d1 d2 d3 d4 d5 d6 d7
                        ; x  y  w  h  dx dy dw num
.elliloop
    movem.w d0-d3,-(a7)

    lsr.w   #2,d0       ; convert x,y,w,h to screen coords
    lsr.w   #2,d1
    lsr.w   #2,d2
    lsr.w   #2,d3

    movem.w d0-d3,-(a7)
    jsr     -180(a6)    ; DrawEllipse(a1:rp,d0-d3:x,y,w,h)
    movem.w (a7)+,d0-d3

    addq    #1,d0       ; x+1 

    movem.w d0-d3,-(a7)
    jsr     -180(a6)    ; DrawEllipse(a1:rp,d0-d3:x,y,w,h)
    movem.w (a7)+,d0-d3

    movem.w (a7)+,d0-d3
    add.w   d4,d0       ; x += delta x
    add.w   d5,d1       ; y += delta y
    add.w   d6,d2       ; w += delta w
    dbf     d7,.elliloop

    move.w  (a4)+,d0    ; negative color = end marker
    bge.b   .drawloop

This worked nice enough, and I realized I might not need palette switching at all: Drawing all the ellipses takes quite some time (you would see blank screen until the final palette is set) and seeing the drawing process in real time builds up a little “Where is this going?” tension.

Tool time!

After some doodling in Photoshop, it was clear I needed a tool to set up the circles. Yay!

Writing interactive drawing tools over and over again is kind of relaxing, like a coding kata. With this tool, I would draw the circle outlines with the mouse and manually edit the delta x, y, width values with a live preview update on each change.

At this point, the dreaded old fight against the compression crept in: Simplifying shapes, leaving out details, and adjusting parameters here and there so the ellipse list would contain more repetitions that the packer can eliminate.

Text lines

Adding the text was straight-forward. I wanted to use orange, so I made sure the last ellipse drawn would set the current pen to orange. After some back and fourth, this emerged as the code structure with the best compression:

    ; After drawing all circles, a4
    ; already points to first text chunk.

    move.l  #300<<16+32,d2  ; x,y in 1 longword

    rept 4
    move.l  a4,a0
    lea     84(a5),a1   ; sc_RastPort
    move.l  d2,36(a1)   ; rp_cp_x and _y
    moveq   #8,d0       ; string length 8
    add.w   d0,a4       ; next 8 characters
    add.w   d0,d2       ; y += 16
    add.w   d0,d2
    jsr     -60(a6)     ; Text(a1:rp,a0:s,d0:length)
    endr

    ...

    include ellipse-data.asm
    dc.w -1
    dc.b 'I CHOO- '
    dc.b '   CHOO-'
    dc.b 'CHOOSE  '
    dc.b '   YOU. '

I could avoid setting the text position with an extra Move call by overwriting the “pen position” in the rastport directly.

It needs sound!

A mere picture without any sound would be lame, but staying in the 512-byte limit might get hard… Sound ideas:

  • Imitate a “choo-choooo” sound (a short and a long sound, fading out in volume)
  • Avoid an explicit waveform and use the start of memory address zero again
  • Find a bearable choo-like combination of audio period and length

The first sound code was 20 bytes over budget:

    move.l  #8<<16+360,$dff0a4  ; length, period
    move.w  #$8001,$dff096      ; enable audio channel 0
    move.l  #$00010002,d2
.choo
    swap    d2                  ; subtract 1.w or 2.w
    move.w  #64,d3
.fade
    move.w  d3,$dff0a8          ; volume
    jsr     -270(a6)            ; WaitTOF (1 tick = 1/50 sec)
    sub.w   d2,d3
    bge.b   .fade
    bra.b   .choo

I experimented a lot with different frequencies and periods…

    move.l  #72<<16+64,$dff0a4
    ;move.l #$000a0500,$dff0a4
    move.l  #$002a0380,$dff0a4
    ;move.l #$000c0400,$dff0a4
    ;move.l #$00080380,$dff0a4
    ;move.l #$000a02e0,$dff0a4
    ;move.l #$000a0200,$dff0a4

…but they all sounded broken, dirty, or painful, and not really like a steam train “chooo”. For the final sound effect, I went for a stereo sound with slightly offset frequencies and periods. Eventually it sounded okay given the constraints!

    move.l  #$00080220,$dff0a4    ; length, period
    move.l  #$000a0200,$dff0a4+16 ; length, period
    move.w  #$8003,$dff096  ; channels 0 and 1
    move.l  #$00040001,d2   ; 4-to-1 length ratio
.choo
    swap    d2
    move.w  #96,d3          ; volume > maximum of 64
.fade
    move.w  d3,$dff0a8      ; aud0vol
    move.w  d3,$dff0a8+16   ; aud1vol
    jsr     _-270(a6)       ; WaitTOF
    sub.w   d2,d3
    bge.b   .fade
    bra.b   .choo

All that extra code required a lot more data wrestling to fit into 512 bytes again. I think the biggest saving came from shifting everything vertically pixel by pixel until one specific value yielded a better overall compressed size.

Final touches

Some tasks were still on the todo list:

  • That mouse cursor was bugging me!
  • Reserve extra space for the decompressed data (avoid overwriting uninitialized memory)
  • Try different pupil and nose positions for a goofier, unhinged look – important! :)
  • Choose Shrinkler or ZX0
  • Test on real hardware

For the mouse pointer, I could shove in a write to disable sprite DMA in the audio loop. This way, the mouse cursor would still be visible during the buildup, but it cost too many bytes doing that right at the beginning.

    move.w  #$0020,$dff096  ; turn off sprite DMA

Extra space for decompression was reserved in the executable hunk header table – we’re system friendly at no cost and don’t overwrite unallocated memory!

While adjusting the facial features I managed to make the face look funnier and save some bytes.

Goofiness variations

Compression-wise, Salvador/ZX0 yielded the best overall size again. Who knows, maybe Shrinkler could have won this time?

PackerCode and dataCompressed sizeTotal size
Shrinkler688 bytes 344 bytes540 bytes
Shrinkler with -b688 bytes 355 bytes544 bytes
Salvador688 bytes 384 bytes512 bytes

I was anxious when it was time to test everything on the real machine, but it did work. Phew! Strangely, though, everything hangs when run without fast RAM or with 1 MB of chip – I still need to investigate that for the final version…

Edit: Found it! The unpack code accidentally still contained a hard-coded fixed destination address. Fixed.

Real hardware choo. Phew!

Downloads

previous next close