21

I came across a series of blog posts about Kaleidoscope, a demo program for the Cromemco Dazzler graphics card. The series implements an emulator for the system so that it could run this code again, which you can see in the page. It was a four-part series, with the last entry explaining the actual workings of the code.

Unfortunately, the author stopped working on his blog before part 4 was posted. In part 3 he describes the basic layout in pseudo-code, leaving the key details as do_something. Being unfamiliar with either the 8080 or Dazzler, figuring that bit out is beyond me.

I'm wondering if anyone out there would be willing to take up the task of decompiling this into a complete pseudo-code description? The code is only ~90 lines long. I'd love to make a Swift+SpriteKit conversion.

4
  • I think it was this Dazzler demo that inspired Dan Silva to make the version that shipped with the Amiga 1000 on the EA Kaleidoscope demo disk.
    – Brian H
    Oct 1, 2022 at 20:46
  • @BrianH That, or the COLOR DEMO that came with Apple's 1979 DOS 3.2 Disk. In fact, I believe the programs on the disk were quite inspired by Cromemco's Dazzler Games pack, as other titles match as well.
    – Raffzahn
    Oct 1, 2022 at 22:27
  • BTW: The do_someting() is by far the most simple part. Just a bit of bit shifting to transform a 32x32 pixel address into a 9 bit memory address and a 1 bit upper/lower pixel selector. Much more intriguing is how he create the pseudo randomness of the waves.
    – Raffzahn
    Oct 2, 2022 at 1:09
  • IIRC I saw a version of this for ZX Spectrum (using attributes only) not sure anymore if it was BASIC or asm (but most likely BASIC) IIRC it was in some magazine like Bit, Zenit or Elektronika ...
    – Spektre
    Oct 2, 2022 at 9:11

3 Answers 3

27

I had a look at the source and added a few comments (*1). Might not be the expected full high level abstract description, but should be still helpful as a first, tiny step to see its workings.


Doing this I think I can understand why the 4th instalment never happened: The program structure might be somewhat alien to today's programmers, expecting neat initialization and complex but clean math, supported by verbose handling of data elements :))

This is none of that. It's short, highly intervened, extreme compact and down to the bare minimum - can't imagine any compiler getting that kind of code when programmed without already implying these optimizations. It may take a second or third reading to see the beauty.


*1 - Bloated that beautiful 90 lines up to 255. Sorry. But it's at least a nice binary FFh complementing the code length of 7Fh :)

13
  • 1
    nice comments +1 I managed to make simple C++ version from your code version have added the code as answer ...
    – Spektre
    Oct 2, 2022 at 9:04
  • 1
    Nice work indeed. Line 15 should be "VID@BF = *2*00H", though, shouldn't it? Oct 2, 2022 at 9:37
  • 1
    @MichaelGraf you're right, that one slipped. Sorry. It was like 6 am when I finished that "oh lets just give it a short look" phase, so I expect a few more details to come up. Regarding structured programming (beside that I do structured assembly since about the same time that programs is dated), the code does quite well adhere to structured programming rules. There are no wild jumps. Spectre's C quite linear adaption proving that point. The comment is more about the way how intervened its data elements are. Anyone having to implement that would use way more variables and interaction.
    – Raffzahn
    Oct 2, 2022 at 12:39
  • 2
    @Raffzahn: Yeah, I get that you would RESET all the time, e.g. just to get a fresh kaleidoscope pattern. But that's a good point about the registers possibly retaining their previous contents on a "warm" reset. I was assuming a "cold" reset where you would tend to expect something fairly consistent, unless the designers deliberately fed them with some kind of noise. Oct 2, 2022 at 16:35
  • 1
    @NateEldredge The 8080 was NMOS and simple. I'm pretty sure I read somewhere (back in the 1970's) that the registers came up random and were also unchanged over a RESET event. I'd have to dig up my original book sets to be sure. (My wife wants me to dump them, very much, and it's possible that I have done so. Must check.)
    – jonk
    Oct 2, 2022 at 20:06
25

Thanks to @Raffzahn commented code (in his answer) I was able to port the code into simple C++ code:

//---------------------------------------------------------------------------
const int VRAM_xs=64,VRAM_ys=64;// resolution
BYTE VRAM[VRAM_ys][VRAM_xs];    // video ram
const DWORD pal[16]=            // color palette (VCL pf32bit format)
    {
    //00BBGGRR
    0x00000000,
    0x00000080,
    0x00008000,
    0x00008080,
    0x00800000,
    0x00800080,
    0x00808000,
    0x00808080,
    0x00000000,
    0x000000FF,
    0x0000FF00,
    0x0000FFFF,
    0x00FF0000,
    0x00FF00FF,
    0x00FFFF00,
    0x00FFFFFF,
    };
BYTE x=0,y=0,m=0;               // Kaleidoscope state
void Kaleidoscope()
    {
    const int xc=VRAM_xs>>1,yc=VRAM_ys>>1;  // center of screen for mirroring
    BYTE c,xx,yy,cc;
    // render
    for (c=0;c<32;c++)
        {
        // update position
        yy=y;
        y+=(x >>2)&m;
        x-=(yy>>2)&m;
        // render 4x mirrored pixels
        xx=x>>3;
        yy=y>>3;
        if (c&1) cc=c>>1; else cc=0;
        VRAM[yc-yy][xc-xx]=cc;
        VRAM[yc-yy][xc+xx]=cc;
        VRAM[yc+yy][xc-xx]=cc;
        VRAM[yc+yy][xc+xx]=cc;
        }
    x++; y++; m++;
    }
//---------------------------------------------------------------------------

You just call the Kaleidoscope() on each frame and then just visualize the content of VRAM[][] in gfx api used... In VCL I did it like this:

void TMain::draw()
    {
    if (!_redraw) return;

    // clear buffer
    bmp->Canvas->Brush->Color=clBlack;
    bmp->Canvas->FillRect(TRect(0,0,xs,ys));

    Kaleidoscope();

    int x,y,xx,yy,xxx,yyy;
    DWORD c;
    for (yy=0,y=0;y<VRAM_ys;y++,yy+=pixel_sz)
     for (xx=0,x=0;x<VRAM_xs;x++,xx+=pixel_sz)
        {
        c=pal[VRAM[y][x]&15];
        for (yyy=yy;yyy<yy+pixel_sz;yyy++)
         for (xxx=xx;xxx<xx+pixel_sz;xxx++)
          pyx[yyy][xxx]=c;
        }

    // render backbuffer
    Main->Canvas->Draw(0,0,bmp);
    _redraw=false;
    }

Where Main is app window and pyx[ys][xs] is direct pixel access to backbuffer bitmap bmp... Also BYTE,DWORD types are unsigned 8 and 32 bit integers so use whatever you have at disposal in case you do not have them or typedef them ...

Here preview:

preview

PS I am not familiar with the Dazzler so The palette I created might be wrong and also I changed the iteration a bit (count also 0 as there where not as many black pixels as colored ones and the image tends to grow too much to my taste)...

18
  • 2
    Looks fine to me (drawing sequence isn't the same, Colour palette is right (assuming it's RGB high to low)). The Blackness is really not that important, as the eye will find patterns anyway. Nice made. Thanks for taking the ugly job of C coding :)) Would be nice to add tat to the repository for completeness.
    – Raffzahn
    Oct 2, 2022 at 12:30
  • 1
    @Raffzahn I found out one difference... you probably got wrong comment ; New X = Last X - ((New Y >> 2) AND MASK) it should be also from Last Y once I repaired it in my code right after // update position comment the output is much nicer (less noisy) I updated both code and preview
    – Spektre
    Oct 2, 2022 at 15:52
  • 1
    Maybe much nicer, but it's not what I see in that code. Calculation of New X starts with the content of A, which still contains New Y, shifted by two (l.101), Masked (l.103) and then subtracted from Last X (l.104..106) to be moved into B, which is X. That is, unless I missed anything (or simply suck at reading 8080.
    – Raffzahn
    Oct 2, 2022 at 16:09
  • 1
    Not sure if I would call it a bug. The original code did work quite well without saving Last Y across the calculation of New Y. So I'd say yes, your code seems to be a useful improvement, but it doesn't reflect the original algorithm. I guess Mr. Wang had multiple constrains here. Also, like any first, having a solution is infinite more valuable than having no solution :))
    – Raffzahn
    Oct 2, 2022 at 16:20
  • 3
    @davidbak hopeless. we're nerds.
    – Raffzahn
    Oct 3, 2022 at 7:51
1

Ok, here is my Swift+SpriteKit version. I'd love it if someone could check the logic in the main updateKalidescope method.

Very annoying: in assembler, adding 200 to a register containing 200 is perfectly legal and you'll get the 144 that you want. The same appears true for the C++ code above? In Swift, you have to clamp the results, because using a UInt8 will get you an overflow. sigh

A curiosity: this is built using Apple's SpriteKit template, which uses SKShapeNode. This gave <1 fps! Changing it to SKSpriteNode gives >60. It seems SKShapeNode is re-drawing (or something) every sprite even if its unchanged.

It's currently updating 60 fps, does that match the Dazzler, or would it have been 30 (or infinity?)

6
  • 1
    Yeah, in C++ unsigned integral types (I'm assuming that's how BYTE is defined) are specifically allowed to overflow. (Not signed types though.) Assembler is of course a free-for-all.
    – davidbak
    Oct 4, 2022 at 17:42
  • The Dazzler version is not synchronized to anything. This is early stuff, way before any modern OS or CPU's fast enough to create speed issues. With the given value ranges and a 2 MHz 8080 it simply adds up to a decent update rate.
    – Raffzahn
    Oct 4, 2022 at 18:35
  • @Raffzahn - if you had to guess, how many loops per second? I set it to 120 in my code and that seems OK. Oct 7, 2022 at 13:43
  • @MauryMarkowitz Seriously, no idea. I did link a video on the Github page showing what seems to be a genuine system (but could also be a faster one). Otherwise one can assume that a 2 MHz 8080 runs as 3-400 kOP/s, so a back of the napkin calculation would take ca 150 instructions (50 main prog+4x25 subroutine) and 64 inner loops per pixel and yield 30-40 (quadruple) pixel set per second. So 120 might be rather high, more what a 4-6 MHz CPU would do :)) Anywhere between one inner loop per two frames (30/s) or one per frame (60/s) might be more appropriate.
    – Raffzahn
    Oct 7, 2022 at 13:56
  • 1
    @davidbak - it turns out there IS a way to do this in Swift, but boy is it NOT obvious! If you want overflow, you add an ampersand - X &+= 1. Ugly, but less so that manually bumping the numbers. BTW, am I right in seeing two values for black in your color list? Oct 7, 2022 at 13:56

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .