diff --git a/.gitignore b/.gitignore
index 100c169..765db6a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ tests/*.s
examples/*.s
!runtime.s
!stdlibwrap.s
+!sprites.s
*.o
*.exe
*.bin
diff --git a/examples/background.pict b/examples/background.pict
new file mode 100644
index 0000000..76b99f6
Binary files /dev/null and b/examples/background.pict differ
diff --git a/examples/sprites.inc b/examples/sprites.inc
new file mode 100644
index 0000000..2b25366
--- /dev/null
+++ b/examples/sprites.inc
@@ -0,0 +1,5 @@
+type SpritePixels = array[0..31] of integer;
+type BackgroundPixels = array[0..31999] of integer;
+
+procedure PutSprite(x,y:integer; var sprite: SpritePixels); external;
+procedure UndrawSprite(x,y:integer; var background: BackgroundPixels); external;
diff --git a/examples/sprites.s b/examples/sprites.s
new file mode 100644
index 0000000..42a3d0e
--- /dev/null
+++ b/examples/sprites.s
@@ -0,0 +1,363 @@
+ .EQU SPRITE_HEIGHT 16
+
+ .EQU WORDS_PER_LINE 80
+ .EQU FB_RA $900
+ .EQU FB_WA $901
+ .EQU FB_IO $902
+ .EQU FB_PS $903
+
+; calculate mask for a word of pixels
+; args: word of pixels with four bits per pixel
+; returns: value that masks out all pixels that are set
+CALC_MASK:
+ LOADC $F ; pixel mask
+C_M_L0:
+ SWAP ; swap mask and pixels value
+ AND.S1.X2Y ; isolate one pixel, keep args
+ CBRANCH.Z C_M_L1 ; if pixel is zero, dont set mask bits
+ OVER ; copy current mask
+ OR ; or into pixels value
+C_M_L1:
+ SWAP ; swap back, ToS is now mask bits
+ SHL 2 ; shift mask for next pixel to the left
+ SHL 2
+
+ DUP
+ CBRANCH.NZ C_M_L0 ; if mask is zero, we are done
+ DROP ; remove mask bits
+ NOT ; invert result
+ RET
+
+; calculate vmem address from coordinates
+; args: x,y
+; returns: vmem word number
+CALC_VMEM_ADDR:
+ ; only works if WORDS_PER_LINE is 80
+ ; and pixels per word is 8
+
+ DUP
+ ; y
+ SHL 2
+ SHL 2
+ SHL 2 ; * 64
+
+ SWAP
+ ; + y
+ SHL 2
+ SHL 2 ; * 16
+ ADD
+
+ SWAP
+ ; word offset = X/8
+ SHR
+ SHR
+ SHR
+ ADD
+
+ RET
+
+; put a sprite on screen
+; arg: x,y pointer to sprite data
+ .EQU PS_VMEM_ADDR 0
+ .EQU PS_SPRITE_DATA 4
+ .EQU PS_SPRITE_LINES 8
+ .EQU PS_X 12
+ .EQU PS_Y 16
+ .EQU PS_SHIFT_C 20
+ .EQU PS_SPILL 24
+ .EQU PS_FS 28
+PUTSPRITE:
+ FPADJ -PS_FS
+ STORE PS_SPRITE_DATA
+ STORE PS_Y
+ STORE PS_X
+
+ ; calculate vmem address
+ LOAD PS_X
+ LOAD PS_Y
+ LOADCP CALC_VMEM_ADDR
+ CALL
+ STORE PS_VMEM_ADDR
+
+ LOAD PS_X ; shift count = x mod 8
+ LOADC 7
+ AND
+ STORE PS_SHIFT_C
+
+ LOADC SPRITE_HEIGHT
+ STORE PS_SPRITE_LINES
+
+ ; loop over each line of the sprite
+PS_LOOP1:
+ ; set read and write address
+ ; in the vga controller
+ LOADC FB_RA ; read address register
+ LOAD PS_VMEM_ADDR
+ STOREI 1 ; use autoincrement to get to the next register
+ LOAD PS_VMEM_ADDR
+ STOREI
+ DROP
+
+ LOAD PS_SPRITE_DATA ; address of sprite data
+ DUP
+ INC 4 ; increment pointer
+ STORE PS_SPRITE_DATA ; and store it again
+ LOADI ; load word from orig. address
+
+
+ LOADC 0
+ STORE PS_SPILL
+
+ ; loop to shift pixel data to right
+ LOAD PS_SHIFT_C ; load shift count
+PS_LOOP2:
+ DUP ; test it for zero
+ CBRANCH.Z PS_LOOP2_X
+
+ SWAP ; swap count with pixels
+
+ ; save the pixel that is shifted out
+ LOADC $F ; mask the four bits
+ AND.S0 ; keep original value on stack
+ BROT ; and move them to MSB
+ BROT
+ BROT
+ SHL 2
+ SHL 2 ; shift by 28 in total
+
+ LOAD PS_SPILL ; load spill bits
+ SHR ; shift by four to make space
+ SHR
+ SHR
+ SHR
+ OR ; or with orig value
+ STORE PS_SPILL ; store new value
+
+ SHR ; shift pixels right
+ SHR ; four bits per pixel
+ SHR
+ SHR
+
+ SWAP ; swap back, count now ToS
+ DEC 1
+ BRANCH PS_LOOP2
+PS_LOOP2_X:
+ DROP ; remove shift count, shifted pixels now in ToS
+
+ DUP
+ LOADCP CALC_MASK ; calculate sprite mask for this word
+ CALL
+
+ LOADCP FB_IO ; address of the i/o register
+ LOADI ; read word from video mem
+
+ AND ; and word with mask
+
+ OR ; OR sprite data with original pixels
+
+ LOADCP FB_IO
+ SWAP
+ STOREI ; store result into i/o reg
+ DROP
+
+ ;
+ ; process spilled bits and right half of sprite data
+ ;
+
+ ; put spill bits on stack for later
+ LOAD PS_SPILL
+
+ LOAD PS_SPRITE_DATA ; address of sprite data
+ DUP
+ INC 4 ; increment pointer
+ STORE PS_SPRITE_DATA ; and store it again
+ LOADI ; load word from orig. address
+
+ ; reset spill bits
+ LOADC 0
+ STORE PS_SPILL
+
+ ; shift pixel data to right
+ LOAD PS_SHIFT_C ; load shift count
+PS_LOOP3: ; test it for zero
+ DUP
+ CBRANCH.Z PS_LOOP3_X
+
+ SWAP ; swap count with pixels
+
+ ; save the pixel that is shifted out
+ LOADC $F ; mask the four bits
+ AND.S0 ; keep original value on stack
+ BROT ; and move them to MSB
+ BROT
+ BROT
+ SHL 2
+ SHL 2 ; shift by 28 in total
+
+ LOAD PS_SPILL ; load spill bits
+ SHR ; shift by four to make space
+ SHR
+ SHR
+ SHR
+ OR ; or with orig value
+ STORE PS_SPILL ; store new value
+
+ SHR ; shift pixels right
+ SHR ; four bits per pixel
+ SHR
+ SHR
+
+ SWAP ; swap back, count now ToS
+ DEC 1
+ BRANCH PS_LOOP3
+PS_LOOP3_X:
+ DROP ; remove shift count, shifted pixels now in ToS
+
+ OR ; or together with spill bits
+
+ DUP
+ LOADCP CALC_MASK ; calculate sprite mask
+ CALL
+
+ LOADCP FB_IO ; load original pixels
+ LOADI
+
+ AND ; and with mask
+
+ OR ; or together with original pixels
+
+ LOADCP FB_IO
+ SWAP
+ STOREI
+ DROP
+
+ ; write spilled bits into next vmem word
+ LOAD PS_SPILL ; get spill bits
+ DUP
+ LOADCP CALC_MASK ; calculate sprite mask for spill bits
+ CALL
+
+ LOADCP FB_IO
+ LOADI ; load next vmem word
+ AND ; apply sprite mask
+
+ OR ; OR in spill bits
+
+ LOADCP FB_IO
+ SWAP ; swap pixels and addr
+ STOREI ; write back
+ DROP
+
+ LOAD PS_SPRITE_LINES ; decrement lines count
+ DEC 1
+ DUP
+ CBRANCH.Z PS_L_XT ; exit if zero
+ STORE PS_SPRITE_LINES
+ ; prepare next line
+ LOAD PS_VMEM_ADDR
+ LOADC WORDS_PER_LINE ; increment to next screen line
+ ADD
+ STORE PS_VMEM_ADDR
+ BRANCH PS_LOOP1
+PS_L_XT:
+ DROP
+
+ FPADJ PS_FS
+ RET
+
+; undraw a sprite, i.e. draw background data
+; over a sprite location
+; args: x,y, ptr to background data
+ .EQU UD_S_X 0
+ .EQU UD_S_Y 4
+ .EQU UD_S_PXS 8
+ .EQU UD_S_BGDATA 12
+ .EQU UD_S_OFFSET 16
+ .EQU UD_S_BGORIG 20
+ .EQU UD_S_FS 24
+UNDRAWSPRITE:
+ FPADJ -UD_S_FS
+ STORE UD_S_BGORIG
+ STORE UD_S_Y
+ STORE UD_S_X
+
+ ; calculate pixel shift
+ LOAD UD_S_X
+ LOADC $7
+ AND
+ STORE UD_S_PXS
+
+ ; calculate vmem offset
+ LOAD UD_S_X
+ LOAD UD_S_Y
+ LOADCP CALC_VMEM_ADDR
+ CALL
+
+ DUP
+ STORE UD_S_OFFSET
+
+ ; calculate background data address from offset
+ SHL 2
+ LOAD UD_S_BGORIG
+ ADD
+ STORE UD_S_BGDATA
+
+ LOADC SPRITE_HEIGHT ; line count
+UD_S_L1:
+ ; store vmem offset into write addr reg
+ LOADCP FB_WA
+ LOAD UD_S_OFFSET
+ STOREI 1 ; ugly but fast: reuse addr
+ ; with postincrement to
+ ; get to FB_IO for STOREI below
+
+ ; load a word of background data
+ LOAD UD_S_BGDATA
+ LOADI
+ ; and write it to vmem
+ STOREI
+ ; reuse addr from STOREI
+
+ ; load 2nd word of background data
+ LOAD UD_S_BGDATA
+ INC 4
+ DUP
+ STORE UD_S_BGDATA
+ LOADI
+ STOREI ; and write it to vmem
+ DROP
+
+ ; if pixel shift is zero, no 3rd word
+ LOAD UD_S_PXS
+ CBRANCH.Z UD_S_L2
+
+ ; load 3rd word of background data
+ LOADCP FB_IO
+ LOAD UD_S_BGDATA
+ INC 4
+ DUP
+ STORE UD_S_BGDATA
+ LOADI
+ STOREI ; and write it to vmem
+ DROP
+
+UD_S_L2:
+ LOAD UD_S_OFFSET
+ LOADCP WORDS_PER_LINE
+ ADD
+ DUP
+ STORE UD_S_OFFSET
+ SHL 2
+ LOAD UD_S_BGORIG
+ ADD
+ STORE UD_S_BGDATA
+
+ DEC 1 ; decrement counter
+ DUP
+ CBRANCH.NZ UD_S_L1 ; check for zero
+
+ DROP ; remove counter
+
+ FPADJ UD_S_FS
+ RET
+
diff --git a/examples/sprites.sprt b/examples/sprites.sprt
new file mode 100644
index 0000000..475bd4d
Binary files /dev/null and b/examples/sprites.sprt differ
diff --git a/examples/xmas.pas b/examples/xmas.pas
new file mode 100644
index 0000000..12b2da3
--- /dev/null
+++ b/examples/xmas.pas
@@ -0,0 +1,220 @@
+program XmasAnimation;
+uses sprites;
+
+type PictData = record
+ magic, mode:integer;
+ palette: array [0..15] of integer;
+ pixeldata: array [0..31999] of integer;
+ end;
+
+ Sprite = record
+ x,y:integer;
+ oldX,oldY:integer;
+ xdelta,ydelta:integer;
+ curFrame:integer;
+ frameCount:integer;
+ frameTime:integer;
+ frameLeft:integer;
+ changed:boolean;
+ frame:array [0..3] of SpritePixels;
+ end;
+
+var pic:PictData;
+ filename:string;
+ infile:file;
+ ch:char;
+ santaSprite: Sprite;
+ deerSprite: Sprite;
+ ohDeerSprite: Sprite;
+ rudolfSprite: Sprite;
+ smokeSprite: Sprite;
+
+procedure WaitVSync; external;
+
+procedure loadPalette(var pic:PictData);
+var i:integer;
+begin
+ for i := 0 to 15 do
+ setpalette(i, pic.palette[i]);
+end;
+
+procedure showPic(var pic:PictData);
+begin
+ PutScreen(pic.pixeldata);
+end;
+
+procedure loadSpriteFrame(var aSprite:Sprite;spriteIndex:integer;
+ var sheetFile:file;sheetIndex:integer);
+begin
+ seek(sheetFile, 8 + sheetIndex * 128);
+ read(sheetFile, aSprite.frame[spriteIndex]);
+ if aSprite.frameCount <= spriteIndex then
+ aSprite.frameCount := spriteIndex + 1;
+
+ aSprite.curFrame := 0;
+ writeln('loaded sprite frame ', spriteIndex, ' from ', sheetIndex);
+end;
+
+procedure animateSprite(var aSprite:Sprite);
+var frameIndex:integer;
+ frameTime,frameLeft:integer;
+ ydelta:integer;
+ oldX,oldY:integer;
+begin
+ ydelta := aSprite.ydelta;
+ frameIndex := aSprite.curFrame;
+ frameTime := aSprite.frameTime;
+ frameLeft := aSprite.frameLeft;
+ oldX := aSprite.x; oldY := aSprite.y;
+ aSprite.oldX := oldX; aSprite.oldY := oldY;
+
+ frameLeft := frameLeft - 1;
+ if frameLeft <= 0 then
+ begin
+ frameIndex := frameIndex + 1;
+ frameLeft := aSprite.frameTime;
+ aSprite.frameLeft := frameLeft;
+ aSprite.curFrame := frameIndex;
+ if frameIndex >= aSprite.frameCount
+ then
+ aSprite.curFrame := 0;
+
+ if frameIndex = 1 then
+ begin
+ ydelta := - ydelta;
+ aSprite.ydelta := ydelta;
+ end;
+ aSprite.y := aSprite.y + ydelta;
+ end;
+
+ aSprite.frameLeft := frameLeft;
+
+ aSprite.x := aSprite.x + aSprite.xdelta;
+ if aSprite.x > 620 then aSprite.x := 0;
+end;
+
+procedure animate;
+var i:integer;
+ ydelta:integer;
+ frameIndex:integer;
+ frameTime:integer;
+ oldX,oldY:integer;
+begin
+ santaSprite.x := 0;
+ santaSprite.y := 60;
+ santaSprite.frameTime := 10;
+ santaSprite.xdelta := 2;
+ santaSprite.ydelta := 1;
+
+ smokeSprite.x := 434;
+ smokeSprite.y := 252;
+ smokeSprite.frameTime := 20;
+
+ deerSprite.x := 18;
+ deerSprite.y := 60;
+ deerSprite.frameTime := 10;
+ deerSprite.xdelta := 2;
+ deerSprite.ydelta := 1;
+
+ ohDeerSprite.x := 33;
+ ohDeerSprite.y := 61;
+ ohDeerSprite.frameTime := 10;
+ ohDeerSprite.xdelta := 2;
+ ohDeerSprite.ydelta := 1;
+
+ rudolfSprite.x := 49;
+ rudolfSprite.y := 60;
+ rudolfSprite.frameTime := 10;
+ rudolfSprite.xdelta := 2;
+ rudolfSprite.ydelta := 1;
+
+ ydelta := 1;
+
+ frameTime := santaSprite.frameTime;
+
+ while not ConAvail do
+ begin
+ frameIndex := santaSprite.curFrame;
+ oldX := santaSprite.x; oldY := santaSprite.y;
+ PutSprite(oldX, oldY, santaSprite.frame[frameIndex]);
+ i := i + 1;
+ frameTime := frameTime - 1;
+ if frameTime = 0 then
+ begin
+ frameTime := santaSprite.frameTime;
+ santaSprite.curFrame := frameIndex + 1;
+ if frameIndex >= santaSprite.frameCount
+ then
+ santaSprite.curFrame := 0;
+
+ if frameIndex = 0 then ydelta := - ydelta;
+ santaSprite.y := santaSprite.y + ydelta;
+ end;
+ santaSprite.x := santaSprite.x + 2;
+ if santaSprite.x > 620 then santaSprite.x := 0;
+
+ PutSprite(deerSprite.x, deerSprite.y,
+ deerSprite.frame[deerSprite.curFrame]);
+
+ PutSprite(ohDeerSprite.x, ohDeerSprite.y,
+ ohDeerSprite.frame[ohDeerSprite.curFrame]);
+
+ PutSprite(rudolfSprite.x, rudolfSprite.y,
+ rudolfSprite.frame[rudolfSprite.curFrame]);
+
+ PutSprite(smokeSprite.x, smokeSprite.y,
+ smokeSprite.frame[smokeSprite.curFrame]);
+
+ animateSprite(deerSprite);
+ animateSprite(ohDeerSprite);
+ animateSprite(rudolfSprite);
+ animateSprite(smokeSprite);
+
+ Delay(10);
+ WaitVSync;
+
+ UndrawSprite(oldX, oldY, pic.pixeldata);
+ UndrawSprite(deerSprite.oldX, deerSprite.oldY, pic.pixeldata);
+ UndrawSprite(ohDeerSprite.oldX, ohDeerSprite.oldY, pic.pixeldata);
+ UndrawSprite(rudolfSprite.oldX, rudolfSprite.oldY, pic.pixeldata);
+ UndrawSprite(smokeSprite.oldX, smokeSprite.oldY, pic.pixeldata);
+ end;
+end;
+
+
+begin
+ filename := 'background.pict';
+ open(infile, filename, ModeReadonly);
+ read(infile, pic);
+ close(infile);
+
+ writeln('magic: ', pic.magic, ' mode:', pic.mode);
+
+ loadPalette(pic);
+ showPic(pic);
+
+ open(infile, 'sprites.sprt', ModeReadOnly);
+ loadSpriteFrame(santaSprite, 0, infile, 0);
+ loadSpriteFrame(santaSprite, 1, infile, 1);
+
+ loadSpriteFrame(deerSprite, 0, infile, 5);
+ loadSpriteFrame(deerSprite, 1, infile, 6);
+ loadSpriteFrame(deerSprite, 2, infile, 7);
+
+ loadSpriteFrame(ohDeerSprite, 0, infile, 7);
+ loadSpriteFrame(ohDeerSprite, 1, infile, 5);
+ loadSpriteFrame(ohDeerSprite, 2, infile, 6);
+
+ loadSpriteFrame(rudolfSprite, 0, infile, 3);
+ loadSpriteFrame(rudolfSprite, 1, infile, 4);
+ loadSpriteFrame(rudolfSprite, 2, infile, 2);
+
+ loadSpriteFrame(smokeSprite, 0, infile, 8);
+ loadSpriteFrame(smokeSprite, 1, infile, 9);
+ loadSpriteFrame(smokeSprite, 2, infile, 10);
+ loadSpriteFrame(smokeSprite, 3, infile, 11);
+
+ close(infile);
+
+ animate;
+end.
diff --git a/tridoracpu/tridoracpu.xpr b/tridoracpu/tridoracpu.xpr
index 5e4a64f..a4c6545 100644
--- a/tridoracpu/tridoracpu.xpr
+++ b/tridoracpu/tridoracpu.xpr
@@ -25,7 +25,7 @@
-
+
diff --git a/utils/png2pict.py b/utils/png2pict.py
new file mode 100644
index 0000000..73c4670
--- /dev/null
+++ b/utils/png2pict.py
@@ -0,0 +1,110 @@
+#!/usr/bin/python3
+import sys
+import png
+
+def process_pixdata(outfile, pixdata, frameindex = 0, pix_w=640, pix_h=400):
+
+ pixmask = 15
+ y = pix_h * frameindex
+ max_y = y + pix_h - 1
+
+ while y <= max_y:
+ x = 0
+ max_x = pix_w - 1
+ pixline = pixdata[y]
+ while x <= max_x:
+ px1 = pixline[x+0] & pixmask
+ px2 = pixline[x+1] & pixmask
+ px3 = pixline[x+2] & pixmask
+ px4 = pixline[x+3] & pixmask
+ px5 = pixline[x+4] & pixmask
+ px6 = pixline[x+5] & pixmask
+ px7 = pixline[x+6] & pixmask
+ px8 = pixline[x+7] & pixmask
+ vmem_word = (px1 << 28) | (px2 << 24) | (px3 << 20) | (px4 << 16) | \
+ (px5 << 12) | (px6 << 8) | (px7 << 4) | px8
+
+ outfile.write(vmem_word.to_bytes(4, 'big'))
+ x += 8
+ y += 1
+
+
+def process_palette(outfile, palette):
+ for r,g,b in palette:
+ r4 = r >> 4
+ g4 = g >> 4
+ b4 = b >> 4
+ c12 = r4 << 8 | g4 << 4 | b4
+
+ outfile.write(c12.to_bytes(4, 'big'))
+
+
+def write_header(outfile):
+ magic = b'PIct'
+ mode = 1
+ outfile.write(magic);
+ outfile.write(mode.to_bytes(4, 'big'))
+
+
+def write_sprite_header(outfile):
+ magic = b'SPRT'
+ mode = 1
+ outfile.write(magic);
+ outfile.write(mode.to_bytes(4, 'big'))
+
+
+def write_pict_file(width, height, px, metadata, outfile):
+ print("processing as PICT file...")
+ if width != 640:
+ print("width must be 640, aborting")
+ sys.exit(1)
+ pixdata = list(px)
+ palette = metadata['palette']
+
+ if len(palette) != 16:
+ print("palette must have 16 colors, aborting")
+ sys.exit(0)
+
+ with open(outfile,'wb') as f:
+ write_header(f)
+ process_palette(f, palette)
+ process_pixdata(f, pixdata)
+
+
+def write_sprite_file(width, height, px, metadata, outfile):
+ sprite_height=16
+
+ print("processing as SPRT file with {} sprites...".format(height//sprite_height))
+ if width != 16:
+ print("width must be 16, aborting")
+ sys.exit(1)
+ pixdata = list(px)
+ palette = metadata['palette']
+
+ if len(palette) != 16:
+ print("palette must have 16 colors, aborting")
+ sys.exit(0)
+
+ with open(outfile,'wb') as f:
+ write_sprite_header(f)
+ process_pixdata(f, pixdata, pix_w=16, pix_h=height)
+
+
+if __name__ == '__main__':
+ if len(sys.argv) != 3:
+ print("Usage: {} ".format(sys.argv[0]))
+ sys.exit(1)
+
+ infile = sys.argv[1]
+ outfile = sys.argv[2]
+
+ r = png.Reader(infile)
+ p = r.read()
+ width, height, px, metadata = p
+ if width == 640:
+ write_pict_file(width, height, px, metadata, outfile)
+ elif width == 16:
+ write_sprite_file(width, height, px, metadata, outfile)
+ else:
+ print("Can't handle this file, need a width of\n640 or 16 pixels with 16 color palette.")
+