From d7a025fd08ed0a1a3179a4c839e86407275ddeb2 Mon Sep 17 00:00:00 2001 From: slederer Date: Mon, 13 Oct 2025 23:33:30 +0200 Subject: [PATCH 01/29] update documentation for October 2025 update --- README.md | 21 ++++++++++++++++++++- doc/mem.md | 1 + 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index be77d37..758c36b 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,25 @@ Other inspirations were, among others, in no particular order: - the Magic-1 by Bill Buzbee - the OPC by revaldinho +## October 2025 Update +This update introduces a data cache for the Tridora-CPU. It is similar to the instruction cache +as it caches the 16 bytes coming from the DRAM memory controller. It is a write-back cache, i.e. +when a word inside the cached area is written, it updates the cache instead of invalidating it. + +This is important because there are many idioms in the stack machine assembly language where you +store a local variable and then read it again (e.g. updating a loop variable). + +Since for most programs, the user stack and parts of the heap are inside the DRAM area, the data cache +has a more noticable impact. In the benchmark program that was already used for the last update, +the data cache results in a 50% improvement for the empty loop test. This is in comparison to the version +without data cache but with the instruction cache, both running code out of DRAM. + +It is also noticable for compile times: With the data cache, compiling and assembling the +"hello,world" program takes 16 seconds instead of 20. With a little tweak of the SD-Card controller +that slightly increased the data transfer rate, the build time goes down to 15 seconds. + +Also, an audio controller was added that allows interrupt-driven sample playback via an AMP2 PMOD. + ## April 2025 Update The clock has been reduced to 77 MHz from 83 MHz. Apparently the design was at the limit and timing problems were cropping up seemingly at random. Reducing the clock speed made some @@ -62,7 +81,7 @@ on the emulator image. - the [Hackaday project](https://hackaday.io/project/198324-tridora-cpu) (mostly copy-paste from this README) - the [YouTube channel](https://www.youtube.com/@tridoracpu/videos) with some demo videos - the [emulator](https://git.insignificance.de/slederer/-/packages/generic/tridoraemu/0.0.5/files/12) (source and windows binary) -- the [FPGA bitstream](https://git.insignificance.de/slederer/-/packages/generic/tdr-bitstream/0.0.2/files/14) for the Arty-A7-35T board +- the [FPGA bitstream](https://git.insignificance.de/slederer/-/packages/generic/tdr-bitstream/0.0.3/files/15) for the Arty-A7-35T board - an [SD-card image](https://git.insignificance.de/slederer/-/packages/generic/tdr-cardimage/0.0.4/files/13) Contact the author here: tridoracpu [at] insignificance.de diff --git a/doc/mem.md b/doc/mem.md index e24fbe2..f7dbc2b 100644 --- a/doc/mem.md +++ b/doc/mem.md @@ -34,3 +34,4 @@ Currently, only I/O slots 0-3 are being used. | 1 | $880 | SPI-SD | | 2 | $900 | VGA | | 3 | $980 | IRQC | +| 4 | $A00 | TDRAUDIO | From 0f72080c56072136be3301530256a2c0ba5bfc3e Mon Sep 17 00:00:00 2001 From: slederer Date: Sun, 26 Oct 2025 00:27:34 +0200 Subject: [PATCH 02/29] tridoracpu: experimented with synthesis options again - workaround for an apparent bug with LOAD address generation at offsets >= 3584 - updated bitstream URL --- README.md | 2 +- tridoracpu/tridoracpu.xpr | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 758c36b..fe930a7 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ on the emulator image. - the [Hackaday project](https://hackaday.io/project/198324-tridora-cpu) (mostly copy-paste from this README) - the [YouTube channel](https://www.youtube.com/@tridoracpu/videos) with some demo videos - the [emulator](https://git.insignificance.de/slederer/-/packages/generic/tridoraemu/0.0.5/files/12) (source and windows binary) -- the [FPGA bitstream](https://git.insignificance.de/slederer/-/packages/generic/tdr-bitstream/0.0.3/files/15) for the Arty-A7-35T board +- the [FPGA bitstream](https://git.insignificance.de/slederer/-/packages/generic/tdr-bitstream/0.0.4/files/16) for the Arty-A7-35T board - an [SD-card image](https://git.insignificance.de/slederer/-/packages/generic/tdr-cardimage/0.0.4/files/13) Contact the author here: tridoracpu [at] insignificance.de diff --git a/tridoracpu/tridoracpu.xpr b/tridoracpu/tridoracpu.xpr index 3767063..30d168a 100644 --- a/tridoracpu/tridoracpu.xpr +++ b/tridoracpu/tridoracpu.xpr @@ -356,12 +356,15 @@ - + - - Vivado Synthesis Defaults + + Performs general area optimizations including changing the threshold for control set optimizations, forcing ternary adder implementation, applying lower thresholds for use of carry chain in comparators and also area optimized mux optimizations. - + + + + @@ -378,14 +381,14 @@ - + - - Similar to Performance_ExplorePostRoutePhysOpt, but enables logic optimization step (opt_design) with the ExploreWithRemap directive. + + Uses multiple algorithms for optimization, placement, and routing to get potentially better results. - + @@ -396,12 +399,9 @@ - - - - + From 87ec71bd6de7a6a649c10be06d16d09b3e264414 Mon Sep 17 00:00:00 2001 From: slederer Date: Wed, 5 Nov 2025 00:30:49 +0100 Subject: [PATCH 03/29] align _END label, add ALIGN directive to assembler - fixes failing memory allocator when _END label is not aligned --- pcomp/emit.pas | 4 +++- pcomp/sasm.pas | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pcomp/emit.pas b/pcomp/emit.pas index a201714..d440951 100644 --- a/pcomp/emit.pas +++ b/pcomp/emit.pas @@ -324,7 +324,9 @@ begin rewindStringList(usedUnits); while nextStringListItem(usedUnits, unitName) do emitInclude(unitName + UnitSuffix2); - + (* _END label needs to be word-aligned because + it is used as the start of the heap *) + emitIns('.ALIGN'); emitLabelRaw('_END'); end; diff --git a/pcomp/sasm.pas b/pcomp/sasm.pas index 1858f11..2af55c3 100644 --- a/pcomp/sasm.pas +++ b/pcomp/sasm.pas @@ -2056,6 +2056,9 @@ begin operandValue := 0; emitBlock(count, operandValue); end + else + if lastToken.tokenText6 = '.ALIGN' then + alignOutput(wordSize) else errorExit2('Unrecognized directive', lastToken.tokenText); end; From 8f4d0176683bef38852d5a2893e651a59666b0eb Mon Sep 17 00:00:00 2001 From: slederer Date: Sun, 30 Nov 2025 23:49:44 +0100 Subject: [PATCH 04/29] sasm: fix typo error; examples: add fire demo --- examples/fastfire.inc | 5 + examples/fastfire.s | 326 ++++++++++++++++++++++++++++++++++++++++++ examples/fire.pas | 76 ++++++++++ examples/fire2.pas | 84 +++++++++++ pcomp/sasm.pas | 2 +- 5 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 examples/fastfire.inc create mode 100644 examples/fastfire.s create mode 100644 examples/fire.pas create mode 100644 examples/fire2.pas diff --git a/examples/fastfire.inc b/examples/fastfire.inc new file mode 100644 index 0000000..bf0dce6 --- /dev/null +++ b/examples/fastfire.inc @@ -0,0 +1,5 @@ +const FIREWIDTH = 319; FIREHEIGHT = 79; (* keep in sync with fastfire.s! *) +type FireBuf = array [0..FIREHEIGHT, 0..FIREWIDTH] of integer; + +procedure FastFireUpdate(var f:FireBuf); external; +procedure FastFireDraw(var f:FireBuf;screenx, screeny:integer); external; diff --git a/examples/fastfire.s b/examples/fastfire.s new file mode 100644 index 0000000..f0e10e4 --- /dev/null +++ b/examples/fastfire.s @@ -0,0 +1,326 @@ + ; width and height of the fire cell matrix + ; Be sure to sync this with fastfire.inc! + .EQU FIREWIDTH 319 + .EQU FIREHEIGHT 79 + + ; + ; The cell matrix actually has one column + ; and one row more than FIREWIDTH and + ; FIREHEIGHT to handle the negative + ; X offsets when calculating new + ; cell values. + ; Likewise, there is one more row. + ; So rows are processed from 0 to FIREHEIGHT - 2 + ; and columms from 1 to FIREWIDTH - 1. + + ; cells considered for calculating new + ; value for cell O (reference cells): + ; .....O...... + ; ....123..... + ; .....4...... + +; args: pointer to fire cell buffer + .EQU FF_ROW_COUNT 0 + .EQU FF_COL_COUNT 4 + .EQU FF_ROW_OFFS 8 + .EQU FF_OFFS1 12 + .EQU FF_OFFS2 16 + .EQU FF_OFFS3 20 + .EQU FF_OFFS4 24 + .EQU FF_CELL_PTR 28 + .EQU FF_FS 32 +FASTFIREUPDATE: + FPADJ -FF_FS + STORE FF_CELL_PTR + LOADC FIREHEIGHT-1 + STORE FF_ROW_COUNT + + ; calculate offsets for reference cells + LOADC FIREWIDTH+1 + SHL 2 + DUP + STORE FF_ROW_OFFS ; offset to next row: WIDTH*4 + DEC 4 ; offset to cell 1: row offset - 4 + DUP + STORE FF_OFFS1 + INC 4 + DUP + STORE FF_OFFS2 ; offset to cell 2: + 4 + INC 4 + STORE FF_OFFS3 ; offset to cell 3: + 4 + LOAD FF_ROW_OFFS + SHL 1 ; offset to cell 4: row offset * 2 + STORE FF_OFFS4 + + ; start at column 1 + LOAD FF_CELL_PTR + INC 4 + STORE FF_CELL_PTR +FF_ROW: + LOADC FIREWIDTH-1 + STORE FF_COL_COUNT + +FF_COL: + LOAD FF_CELL_PTR + LOAD FF_OFFS1 + ADD + LOADI + + LOAD FF_CELL_PTR + LOAD FF_OFFS2 + ADD + LOADI + + LOAD FF_CELL_PTR + LOAD FF_OFFS3 + ADD + LOADI + + LOAD FF_CELL_PTR + LOAD FF_OFFS4 + ADD + LOADI + + ADD + ADD + ADD + + SHR + SHR + + ; if new cell value > 0, subtract 1 to cool down + DUP + CBRANCH.Z FF_SKIP + DEC 1 +FF_SKIP: + LOAD FF_CELL_PTR ; load cell ptr + SWAP ; swap with new value + STOREI 4 ; store with postincrement + STORE FF_CELL_PTR ; save new ptr value + + LOAD FF_COL_COUNT ; decrement column count + DEC 1 + DUP + STORE FF_COL_COUNT + CBRANCH.NZ FF_COL ; loop if col count <> 0 + + ; at the end of a row, go to next row + ; by adding 8 to the cell pointer, + ; skipping the first cell of the next row + LOAD FF_CELL_PTR + INC 8 + STORE FF_CELL_PTR + + LOAD FF_ROW_COUNT ; decrement row count + DEC 1 + DUP + STORE FF_ROW_COUNT + CBRANCH.NZ FF_ROW ; loop if row count <> 0 + +FF_EXIT: + FPADJ FF_FS + RET + +; framebuffer controller registers + .EQU FB_RA $900 + .EQU FB_WA $901 + .EQU FB_IO $902 + .EQU FB_PS $903 + .EQU FB_PD $904 + .EQU FB_CTL $905 + .EQU WORDS_PER_LINE 80 + +; fire width in vmem words (strict left-to-right evaluation) + .EQU FFD_ROW_WORDS 1 + FIREWIDTH / 8 + +; draw all fire cells +; args: pointer to fire cell buffer, screen x, screen y + .EQU FFD_CELL_PTR 0 + .EQU FFD_X 4 + .EQU FFD_Y 8 + .EQU FFD_ROW_COUNT 12 + .EQU FFD_ROW_WORDCOUNT 16 + .EQU FFD_VMEM_PTR 20 + .EQU FFD_FS 24 +FASTFIREDRAW: + FPADJ -FFD_FS + STORE FFD_Y + STORE FFD_X + STORE FFD_CELL_PTR + + ; calculate video memory addr + ; addr = y * 80 + X / 8 + LOAD FFD_Y + SHL 2 ; y * 16 + SHL 2 + DUP + SHL 2 ; + y * 64 + ADD ; = y * 80 + + LOAD FFD_X + SHR + SHR + SHR + ADD ; + x / 8 + + DUP + STORE FFD_VMEM_PTR + LOADC FB_WA ; set vmem write address + SWAP + STOREI + DROP + + LOADC FIREHEIGHT + 1 + STORE FFD_ROW_COUNT +FFD_ROW: + LOADC FFD_ROW_WORDS + STORE FFD_ROW_WORDCOUNT + + LOADC FB_WA ; set vmem write address + LOAD FFD_VMEM_PTR + STOREI + DROP + +FFD_WORD: + LOAD FFD_CELL_PTR ; load cell ptr + LOADC 0 ; vmem word, start with 0 + + ; leftmost pixel (0) + OVER ; [ cptr, vmemw, cptr ] + LOADI ; load cell value [ cptr, vmemw, cellval ] + SHR ; scale it down (from 7 bits to 4) + SHR + SHR ; [ cptr, vmemw, cellval shr 3 ] + OR ; [ cptr, vmemw ] + SWAP ; [ vmemw, cptr ] + INC 4 ; increment cell ptr on stack [ vmemw, cptr + 4 ] + SWAP ; [ cptr + 4, vmemw ] + + SHL 2 ; move bits to left for next pixel + SHL 2 + + ; pixel 1 + OVER + LOADI ; load cell value + SHR ; scale it down (from 7 bits to 4) + SHR + SHR + OR + SWAP + INC 4 ; increment cell ptr on stack + SWAP + + SHL 2 ; move bits to left for next pixel + SHL 2 + + ; pixel 2 + OVER + LOADI ; load cell value + SHR ; scale it down (from 7 bits to 4) + SHR + SHR + OR + SWAP + INC 4 ; increment cell ptr on stack + SWAP + + SHL 2 ; move bits to left for next pixel + SHL 2 + + ; pixel 3 + OVER + LOADI ; load cell value + SHR ; scale it down (from 7 bits to 4) + SHR + SHR + OR + SWAP + INC 4 ; increment cell ptr on stack + SWAP + + SHL 2 ; move bits to left for next pixel + SHL 2 + + ; pixel 4 + OVER + LOADI ; load cell value + SHR ; scale it down (from 7 bits to 4) + SHR + SHR + OR + SWAP + INC 4 ; increment cell ptr on stack + SWAP + + SHL 2 ; move bits to left for next pixel + SHL 2 + + ; pixel 5 + OVER + LOADI ; load cell value + SHR ; scale it down (from 7 bits to 4) + SHR + SHR + OR + SWAP + INC 4 ; increment cell ptr on stack + SWAP + + SHL 2 ; move bits to left for next pixel + SHL 2 + + ; pixel 6 + OVER + LOADI ; load cell value + SHR ; scale it down (from 7 bits to 4) + SHR + SHR + OR + SWAP + INC 4 ; increment cell ptr on stack + SWAP + + SHL 2 ; move bits to left for next pixel + SHL 2 + + ; pixel 7 + OVER + LOADI ; load cell value + SHR ; scale it down (from 7 bits to 4) + SHR + SHR + OR + SWAP + INC 4 ; increment cell ptr on stack + SWAP + + ; store word to vmem + ; vmem write addr will autoincrement + LOADC FB_IO + SWAP + STOREI + DROP + + STORE FFD_CELL_PTR + + ; prepare for next word + LOAD FFD_ROW_WORDCOUNT + DEC 1 + DUP + STORE FFD_ROW_WORDCOUNT + CBRANCH.NZ FFD_WORD + + ; prepare for next row + LOAD FFD_VMEM_PTR + LOADC WORDS_PER_LINE + ADD + STORE FFD_VMEM_PTR + + LOAD FFD_ROW_COUNT + DEC 1 + DUP + STORE FFD_ROW_COUNT + CBRANCH.NZ FFD_ROW +FFD_EXIT: + FPADJ FFD_FS + RET diff --git a/examples/fire.pas b/examples/fire.pas new file mode 100644 index 0000000..22f0217 --- /dev/null +++ b/examples/fire.pas @@ -0,0 +1,76 @@ +{$H1} +{$S2} +program fire; +const MAXX = 30; + MAXY = 50; +var firebuf: array [0..MAXY, 0..MAXX] of integer; + + firepalette: array [0..15] of integer = + ( $FFA, $FF8, $FF4, $FF0, $FE0, $FD0, $FA0, $F90, + $F00, $E00, $D00, $A00, $800, $600, $300, $000); + x,y:integer; + +procedure createPalette; +var i:integer; +begin + for i := 15 downto 0 do + setpalette(15 - i, firepalette[i]); +end; + +procedure fireItUp; +var x,y:integer; +begin + y := MAXY - 1; + for x := 1 to MAXX - 1 do + firebuf[y, x] := random and 127; +end; + +procedure updateFire; +var i,x,y:integer; +begin + for y := 0 to MAXY - 2 do + for x := 1 to MAXX - 1 do + begin + i := + ((firebuf[y + 1, x - 1] + + firebuf[y + 1, x] + + firebuf[y + 1, x + 1] + + firebuf[y + 2, x]) + ) shr 2; + if i > 0 then + i := i - 1; + firebuf[y, x] := i; + end; +end; + +procedure drawFire; +var x, y, col:integer; +begin + for y := 0 to MAXY - 1 do + begin + x := 0; + for col in firebuf[y] do + begin + putpixel(300 + x, 150 + y, col shr 3); + x := x + 1; + end; + end; +end; + +begin + randomize; + initgraphics; + createPalette; + while not conavail do + begin + fireItUp; + updateFire; + drawFire; + end; + + for y := 0 to MAXY do + begin + x := firebuf[y, 10]; + drawline(0, y, x, y, 1); + end; +end. diff --git a/examples/fire2.pas b/examples/fire2.pas new file mode 100644 index 0000000..72fb254 --- /dev/null +++ b/examples/fire2.pas @@ -0,0 +1,84 @@ +{$H1} +{$S1} +program fire2; +uses fastfire; + +const MAXX = FIREWIDTH; + MAXY = FIREHEIGHT; + +var firecells: FireBuf; + + firepalette: array [0..15] of integer = + { ( $FFA, $FF8, $FF4, $FF0, $FE0, $FD0, $FA0, $F90, + $F00, $E00, $D00, $A00, $800, $600, $300, $000); } + ( $FFA, $FFA, $FFA, $FFA, $FF0, $FF0, $FF0, $FF0, + $FF0, $FD0, $FA0, $C00, $A00, $700, $400, $000); + x,y:integer; + +procedure createPalette; +var i:integer; +begin + for i := 15 downto 0 do + setpalette(15 - i, firepalette[i]); +end; + +procedure fireItUp; +var x,y:integer; +begin + y := MAXY - 1; + for x := 1 to MAXX - 1 do + firecells[y, x] := random and 127; +end; + + +procedure updateFire; +var i,x,y:integer; +begin + for y := 0 to MAXY - 2 do + for x := 1 to MAXX - 1 do + begin + i := + ((firecells[y + 1, x - 1] + + firecells[y + 1, x] + + firecells[y + 1, x + 1] + + firecells[y + 2, x]) + ) shr 2; + if i > 0 then + i := i - 1; + firecells[y, x] := i; + end; +end; + +procedure drawFire; +var x, y, col:integer; +begin + for y := 0 to MAXY - 1 do + begin + x := 0; + for col in firecells[y] do + begin + putpixel(100 + x, 150 + y, col shr 3); + x := x + 1; + end; + end; +end; + +begin + randomize; + initgraphics; + createPalette; + while not conavail do + begin + fireItUp; + FastFireUpdate(firecells); + { updateFire; } + FastFireDraw(firecells, 160, 100); + { drawFire; } + end; + + for y := 0 to MAXY do + begin + x := firecells[y, 10]; + drawline(0, y, x, y, 1); + end; +end. diff --git a/pcomp/sasm.pas b/pcomp/sasm.pas index 2af55c3..d032748 100644 --- a/pcomp/sasm.pas +++ b/pcomp/sasm.pas @@ -2057,7 +2057,7 @@ begin emitBlock(count, operandValue); end else - if lastToken.tokenText6 = '.ALIGN' then + if lastToken.tokenText = '.ALIGN' then alignOutput(wordSize) else errorExit2('Unrecognized directive', lastToken.tokenText); From 0016d4ea25d95614417f1334c58fd9571052a809 Mon Sep 17 00:00:00 2001 From: slederer Date: Fri, 5 Dec 2025 00:58:15 +0100 Subject: [PATCH 05/29] utils/serload: add interactive mode xfer: reset block count on transfer start --- progs/xfer.pas | 1 + utils/serload.py | 151 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 110 insertions(+), 42 deletions(-) diff --git a/progs/xfer.pas b/progs/xfer.pas index 13a7cc2..0d871d2 100644 --- a/progs/xfer.pas +++ b/progs/xfer.pas @@ -226,6 +226,7 @@ begin if not invalid then begin open(xferFile, filename, ModeOverwrite); + blockNo := 0; done := false; repeat serReadBlock(ok); diff --git a/utils/serload.py b/utils/serload.py index 6ccc4a6..e69837f 100644 --- a/utils/serload.py +++ b/utils/serload.py @@ -16,6 +16,7 @@ # limitations under the License. import sys +import os import serial import time import random @@ -41,30 +42,6 @@ def get_default_device(): return '/dev/ttyUSB1' -def serwrite_slow(databytes, ser): - total = len(data) - count = 1 - for d in data: - sys.stdout.write("writing {0:02x} {1:04d}/{2:04d}\r".format(ord(d), count, total)) - ser.write(bytes(d,"utf8")) - count += 1 - time.sleep(0.020) - print() - - -def serwrite(datafile, ser): - with open(datafile) as f: - data = f.read() - total = len(data) - count = 1 - for d in data: - sys.stdout.write("writing {0:02x} {1:04d}/{2:04d}\r".format(ord(d), count, total)) - ser.write(bytes(d,"utf8")) - count += 1 - time.sleep(0.020) - print() - - def checksum(databytes): i = 0 cksum = 0 @@ -85,10 +62,26 @@ def sendchar(char, ser): ser.write(char.to_bytes(1, 'big')) -def sendcommand(ser, cmd=b'L'): +def sendcommand(ser, cmd=b'L', verbose=False): + verbose = True ser.write(cmd) resp = ser.read_until() - print(cmd,"sent, response:", str(resp)) + if verbose: + print(cmd,"sent, response:", str(resp)) + return resp + + +# send command and wait for echo +def commandwait(ser, cmd): + resp = sendcommand(ser, cmd, verbose=False) + if len(resp) == 0: + print("timeout sending '{}' command".format(cmd)) + return None + + if resp != bytearray(cmd + b"\r\n"): + print("invalid response to '{}' command".format(cmd)) + return None + return resp @@ -153,6 +146,8 @@ def serload_bin(datafile, ser): data += bytearray(pad) + print("{} total blocks".format((len(data) + blocksize - 1) // blocksize)) + if not send_size_header(ser, filesize): print("Error sending size header.") return @@ -279,18 +274,8 @@ def serdownload(fname, ser): def mput(filenames, ser): for f in filenames: - f_encoded = f.encode('utf8') - print("Setting filename", f) - resp = sendcommand(ser, b'S') - if len(resp) == 0: - print("timeout sending 'S' command") - return - if resp != b'S\r\n' and resp != b'> S\r\n': - print("unrecognized response to 'S' command, aborting") - return - resp = sendcommand(ser, f_encoded + b'\r') - if not f_encoded in resp: - print("unrecognized response to filename, aborting") + resp = set_filename(f, ser) + if resp is None: return serload_bin(f, ser) @@ -299,12 +284,92 @@ def mput(filenames, ser): time.sleep(2) +def set_filename(f, ser): + f_encoded = f.encode('utf8') + print("Setting filename", f) + resp = commandwait(ser, b'S') + if resp is None: + return None + resp = sendcommand(ser, f_encoded + b'\r') + if not f_encoded in resp: + print("unrecognized response to filename, aborting") + return None + return resp + + +def getnamedfile(filename, ser): + resp = set_filename(filename, ser) + if resp is None: + return None + serdownload(filename, ser) + + +def putnamedfile(filename, ser): + resp = set_filename(filename, ser) + if resp is None: + return None + serload_bin(filename, ser) + print("Remote status:") + showdata(ser) + + +def showdata(ser): + + promptseen = False + + while not promptseen: + c = ser.read(1) + if c == b'>': + promptseen = True + else: + print(c.decode('utf8'), end='') + rest = ser.read(1) + + +def localdir(): + result = os.walk(".") + for dirpath, dirnames, filenames in os.walk("."): + for f in filenames: + print(f) + break + + +def interactive(ser): + done = False + while not done: + args = input("> ").strip().split() + if len(args) > 0: + cmd = args[0] + args.pop(0) + if cmd == 'dir': + if commandwait(ser, b'Y') is None: + return + showdata(ser) + elif cmd == 'get': + if len(args) > 1: + print("exactly one argument required (filename)") + else: + getnamedfile(args[0], ser) + elif cmd == 'put': + if len(args) > 1: + print("exactly one argument required (filename)") + else: + putnamedfile(args[0], ser) + elif cmd == 'ldir': + if len(args) > 0: + print("superfluous argument") + else: + localdir() + else: + print("Unknown command. Valid commands are: dir get ldir put") + + if __name__ == "__main__": argparser = argparse.ArgumentParser( description='transfer files from/to the Tridora-CPU') argparser.add_argument('-d', '--device', help='serial device', default=get_default_device()) - argparser.add_argument('command', choices=['get', 'put', 'mput']) - argparser.add_argument('filename', nargs='+') + argparser.add_argument('command', choices=['get', 'put', 'mput', 'interactive']) + argparser.add_argument('filename', nargs='*') args = argparser.parse_args() cmd = args.command @@ -319,8 +384,10 @@ if __name__ == "__main__": serload_bin(filenames[0], ser) elif cmd == 'mput': mput(filenames, ser) + elif cmd == 'interactive': + interactive(ser) else: print("should not get here") - if cmd is not None: - ser.close() + #if cmd is not None: + # ser.close() From d2f3b09e72e1990dc38c5c643b9cf7446c0f70b7 Mon Sep 17 00:00:00 2001 From: slederer Date: Mon, 15 Dec 2025 00:53:36 +0100 Subject: [PATCH 06/29] tridoracpu: cleaned up top a bit, removed some warnings --- .../tridoracpu.srcs/Arty-A7-35-Master.xdc | 6 ++-- tridoracpu/tridoracpu.srcs/stackcpu.v | 23 +++++-------- tridoracpu/tridoracpu.srcs/tdraudio.v | 22 +++++++++---- tridoracpu/tridoracpu.srcs/top.v | 21 ++++++------ tridoracpu/tridoracpu.xpr | 33 +++++++------------ 5 files changed, 47 insertions(+), 58 deletions(-) diff --git a/tridoracpu/tridoracpu.srcs/Arty-A7-35-Master.xdc b/tridoracpu/tridoracpu.srcs/Arty-A7-35-Master.xdc index 2a33ae0..d2c3160 100644 --- a/tridoracpu/tridoracpu.srcs/Arty-A7-35-Master.xdc +++ b/tridoracpu/tridoracpu.srcs/Arty-A7-35-Master.xdc @@ -8,8 +8,8 @@ set_property -dict {PACKAGE_PIN E3 IOSTANDARD LVCMOS33} [get_ports clk] create_clock -period 10.000 -name sys_clk_pin -waveform {0.000 5.000} -add [get_ports clk] ## Switches -set_property -dict {PACKAGE_PIN A8 IOSTANDARD LVCMOS33} [get_ports sw0] -set_property -dict {PACKAGE_PIN C11 IOSTANDARD LVCMOS33} [get_ports sw1] +#set_property -dict {PACKAGE_PIN A8 IOSTANDARD LVCMOS33} [get_ports sw0] +#set_property -dict {PACKAGE_PIN C11 IOSTANDARD LVCMOS33} [get_ports sw1] #set_property -dict { PACKAGE_PIN C10 IOSTANDARD LVCMOS33 } [get_ports { sw[2] }]; #IO_L13N_T2_MRCC_16 Sch=sw[2] #set_property -dict { PACKAGE_PIN A10 IOSTANDARD LVCMOS33 } [get_ports { sw[3] }]; #IO_L14P_T2_SRCC_16 Sch=sw[3] @@ -34,7 +34,7 @@ set_property -dict {PACKAGE_PIN T9 IOSTANDARD LVCMOS33} [get_ports led2] set_property -dict {PACKAGE_PIN T10 IOSTANDARD LVCMOS33} [get_ports led3] ## Buttons -set_property -dict {PACKAGE_PIN D9 IOSTANDARD LVCMOS33} [get_ports btn0] +#set_property -dict {PACKAGE_PIN D9 IOSTANDARD LVCMOS33} [get_ports btn0] #set_property -dict { PACKAGE_PIN C9 IOSTANDARD LVCMOS33 } [get_ports { btn1 }]; #IO_L11P_T1_SRCC_16 Sch=btn[1] #set_property -dict { PACKAGE_PIN B9 IOSTANDARD LVCMOS33 } [get_ports { btn[2] }]; #IO_L11N_T1_SRCC_16 Sch=btn[2] #set_property -dict { PACKAGE_PIN B8 IOSTANDARD LVCMOS33 } [get_ports { btn[3] }]; #IO_L12P_T1_MRCC_16 Sch=btn[3] diff --git a/tridoracpu/tridoracpu.srcs/stackcpu.v b/tridoracpu/tridoracpu.srcs/stackcpu.v index 33b58ec..1d929f7 100644 --- a/tridoracpu/tridoracpu.srcs/stackcpu.v +++ b/tridoracpu/tridoracpu.srcs/stackcpu.v @@ -16,11 +16,11 @@ module stackcpu #(parameter ADDR_WIDTH = 32, WIDTH = 32, output wire write_enable, input wire mem_wait, - output wire led1, - output wire led2, - output wire led3 + output wire debug1, + output wire debug2, + output wire debug3 ); - + localparam EVAL_STACK_INDEX_WIDTH = 6; wire reset = !rst; @@ -90,7 +90,6 @@ module stackcpu #(parameter ADDR_WIDTH = 32, WIDTH = 32, wire mem_write; wire x_is_zero; - // wire [WIDTH-1:0] y_plus_operand = Y + operand; wire x_equals_y = X == Y; wire y_lessthan_x = $signed(Y) < $signed(X); @@ -105,16 +104,10 @@ module stackcpu #(parameter ADDR_WIDTH = 32, WIDTH = 32, assign write_enable = mem_write_enable; // debug output ------------------------------------------------------------------------------------ - assign led1 = reset; - assign led2 = ins_loadc; - assign led3 = ins_branch; -// assign debug_out1 = { mem_read_enable, mem_write_enable, x_is_zero, -// ins_branch, ins_aluop, y_lessthan_x, x_equals_y, {7{1'b0}}, seq_state}; -// assign debug_out2 = data_in; -// assign debug_out3 = nX; -// assign debug_out4 = nPC; -// assign debug_out5 = ins; -// assign debug_out6 = IV; + assign debug1 = reset; + assign debug2 = ins_loadc; + assign debug3 = ins_branch; + //-------------------------------------------------------------------------------------------------- // instruction decoding diff --git a/tridoracpu/tridoracpu.srcs/tdraudio.v b/tridoracpu/tridoracpu.srcs/tdraudio.v index 0cc055e..1629e31 100644 --- a/tridoracpu/tridoracpu.srcs/tdraudio.v +++ b/tridoracpu/tridoracpu.srcs/tdraudio.v @@ -7,7 +7,7 @@ module wavegen #(DATA_WIDTH=32, CLOCK_DIV_WIDTH=22, input wire reset, input wire [1:0] reg_sel, output wire [DATA_WIDTH-1:0] rd_data, - input wire [DATA_WIDTH-1:0] wr_data, + input wire [AMP_WIDTH-1:0] wr_data, input wire rd_en, input wire wr_en, @@ -20,6 +20,9 @@ module wavegen #(DATA_WIDTH=32, CLOCK_DIV_WIDTH=22, localparam TDRAU_REG_CLK = 1; /* clock divider register */ localparam TDRAU_REG_AMP = 2; /* amplitude (volume) register */ + /* avoid warning about unconnected port */ + (* keep="soft" *) wire _unused = rd_en; + reg channel_enable; reg [CLOCK_DIV_WIDTH-1:0] clock_div; reg [CLOCK_DIV_WIDTH-1:0] div_count; @@ -29,12 +32,12 @@ module wavegen #(DATA_WIDTH=32, CLOCK_DIV_WIDTH=22, wire fifo_wr_en; wire fifo_rd_en, fifo_full, fifo_empty; - wire [DATA_WIDTH-1:0] fifo_rd_data; + wire [AMP_WIDTH-1:0] fifo_rd_data; fifo #(.ADDR_WIDTH(4), .DATA_WIDTH(16)) sample_buf( clk, reset, fifo_wr_en, fifo_rd_en, - wr_data, fifo_rd_data, + wr_data[AMP_WIDTH-1:0], fifo_rd_data, fifo_full, fifo_empty ); @@ -166,9 +169,14 @@ module tdraudio #(DATA_WIDTH=32) ( localparam AMP_BIAS = 32768; localparam DAC_WIDTH = 18; + /* avoid warning about unconnected port */ + (* keep="soft" *) wire [DATA_WIDTH-1:AMP_WIDTH] _unused = wr_data[DATA_WIDTH-1:AMP_WIDTH]; + wire [4:0] chan_sel = io_addr[6:2]; wire [1:0] reg_sel = io_addr[1:0]; + wire [AMP_WIDTH-1:0] amp_wr_data = wr_data[AMP_WIDTH-1:0]; + wire [AMP_WIDTH-1:0] chan0_amp; wire [DATA_WIDTH-1:0] chan0_rd_data; wire chan0_running; @@ -210,25 +218,25 @@ module tdraudio #(DATA_WIDTH=32) ( {DATA_WIDTH{1'b1}}; wavegen chan0(clk, reset, reg_sel, - chan0_rd_data, wr_data, + chan0_rd_data, amp_wr_data, chan0_rd_en, chan0_wr_en, chan0_amp, chan0_running, chan0_irq); wavegen chan1(clk, reset, reg_sel, - chan1_rd_data, wr_data, + chan1_rd_data, amp_wr_data, chan1_rd_en, chan1_wr_en, chan1_amp, chan1_running, chan1_irq); wavegen chan2(clk, reset, reg_sel, - chan2_rd_data, wr_data, + chan2_rd_data, amp_wr_data, chan2_rd_en, chan2_wr_en, chan2_amp, chan2_irq, chan2_running); wavegen chan3(clk, reset, reg_sel, - chan3_rd_data, wr_data, + chan3_rd_data, amp_wr_data, chan3_rd_en, chan3_wr_en, chan3_amp, chan3_running, chan3_irq); diff --git a/tridoracpu/tridoracpu.srcs/top.v b/tridoracpu/tridoracpu.srcs/top.v index 6a70ef0..a4533d2 100644 --- a/tridoracpu/tridoracpu.srcs/top.v +++ b/tridoracpu/tridoracpu.srcs/top.v @@ -15,9 +15,6 @@ module top( input wire clk, input wire rst, - input wire btn0, - input wire sw0, - input wire sw1, output wire led0, output wire led1, output wire led2, @@ -229,6 +226,15 @@ module top( assign uart_rd_data = { {WIDTH-10{1'b1}}, uart_rx_avail, uart_tx_busy, uart_rx_data }; wire audio_irq; + + buart #(.CLKFREQ(`clkfreq)) uart0(`clock, rst, + uart_baud, + uart_txd_in, uart_rxd_out, + uart_rx_clear, uart_tx_en, + uart_rx_avail, uart_tx_busy, + uart_tx_data, uart_rx_data); + + // audio controller `ifdef ENABLE_TDRAUDIO wire [WIDTH-1:0] tdraudio_wr_data; wire [WIDTH-1:0] tdraudio_rd_data; @@ -273,13 +279,6 @@ module top( `endif -1; - buart #(.CLKFREQ(`clkfreq)) uart0(`clock, rst, - uart_baud, - uart_txd_in, uart_rxd_out, - uart_rx_clear, uart_tx_en, - uart_rx_avail, uart_tx_busy, - uart_tx_data, uart_rx_data); - // CPU ----------------------------------------------------------------- stackcpu cpu0(.clk(`clock), .rst(rst), .irq(irq), .addr(mem_addr), @@ -287,7 +286,7 @@ module top( .read_ins(dram_read_ins), .data_out(mem_write_data), .write_enable(mem_write_enable), .mem_wait(mem_wait), - .led1(led1), .led2(led2), .led3(led3)); + .debug1(led1), .debug2(led2), .debug3(led3)); // Interrupt Controller irqctrl irqctrl0(`clock, irq_in, irqc_cs, mem_write_enable, diff --git a/tridoracpu/tridoracpu.xpr b/tridoracpu/tridoracpu.xpr index 30d168a..a9dc20f 100644 --- a/tridoracpu/tridoracpu.xpr +++ b/tridoracpu/tridoracpu.xpr @@ -356,15 +356,12 @@ - + - - Performs general area optimizations including changing the threshold for control set optimizations, forcing ternary adder implementation, applying lower thresholds for use of carry chain in comparators and also area optimized mux optimizations. + + Vivado Synthesis Defaults - - - - + @@ -381,26 +378,18 @@ - + - - Uses multiple algorithms for optimization, placement, and routing to get potentially better results. + + Default settings for Implementation. - - - + - - - + - - - - - - + + From a9412d1339d16114e85b75c1478cac4fcf2ccb57 Mon Sep 17 00:00:00 2001 From: slederer Date: Thu, 1 Jan 2026 02:07:36 +0100 Subject: [PATCH 07/29] tdraudio: fix wiring for channel 2, irqctrl: increase delay --- tridoracpu/tridoracpu.srcs/irqctrl.v | 2 +- tridoracpu/tridoracpu.srcs/tdraudio.v | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tridoracpu/tridoracpu.srcs/irqctrl.v b/tridoracpu/tridoracpu.srcs/irqctrl.v index b71df60..5608440 100644 --- a/tridoracpu/tridoracpu.srcs/irqctrl.v +++ b/tridoracpu/tridoracpu.srcs/irqctrl.v @@ -1,6 +1,6 @@ `timescale 1ns / 1ps -module irqctrl #(IRQ_LINES = 3, IRQ_DELAY_WIDTH = 4) ( +module irqctrl #(IRQ_LINES = 3, IRQ_DELAY_WIDTH = 8) ( input wire clk, input wire [IRQ_LINES-1:0] irq_in, input wire cs, diff --git a/tridoracpu/tridoracpu.srcs/tdraudio.v b/tridoracpu/tridoracpu.srcs/tdraudio.v index 1629e31..4ad978d 100644 --- a/tridoracpu/tridoracpu.srcs/tdraudio.v +++ b/tridoracpu/tridoracpu.srcs/tdraudio.v @@ -233,7 +233,7 @@ module tdraudio #(DATA_WIDTH=32) ( chan2_rd_data, amp_wr_data, chan2_rd_en, chan2_wr_en, chan2_amp, - chan2_irq, chan2_running); + chan2_running, chan2_irq); wavegen chan3(clk, reset, reg_sel, chan3_rd_data, amp_wr_data, From caa07474f8574ad7cc088587bf50326ab79a0c53 Mon Sep 17 00:00:00 2001 From: slederer Date: Thu, 1 Jan 2026 02:09:02 +0100 Subject: [PATCH 08/29] minor comment/documentation cleanups --- doc/uart.md | 2 +- tridoracpu/tridoracpu.srcs/stackcpu.v | 5 ++++- tridoracpu/tridoracpu.srcs/top.v | 7 +++---- tridoracpu/tridoracpu.xpr | 12 +++--------- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/doc/uart.md b/doc/uart.md index b349eb4..6a6d191 100644 --- a/doc/uart.md +++ b/doc/uart.md @@ -37,7 +37,7 @@ It uses a fixed serial configuration of 115200 bps, 8 data bits, 1 stop bit, no ## Notes -A 16 byte FIFO is used when receiving data. +A 64 byte FIFO is used when receiving data. When reading data, each byte needs to be acknowledged by writing the _C_ flag to the UART register. diff --git a/tridoracpu/tridoracpu.srcs/stackcpu.v b/tridoracpu/tridoracpu.srcs/stackcpu.v index 1d929f7..b8ef78c 100644 --- a/tridoracpu/tridoracpu.srcs/stackcpu.v +++ b/tridoracpu/tridoracpu.srcs/stackcpu.v @@ -399,7 +399,10 @@ module stackcpu #(parameter ADDR_WIDTH = 32, WIDTH = 32, // process irq always @(posedge clk) begin - if(seq_state == MEM && irq_pending && !(ins_xfer & xfer_r2p)) // in FETCH state, clear irq_pending. + // in MEM state, clear irq_pending, when nPC has been set to IV + // RET instruction is a special case because we need to use + // the new PC that is in mem_data + if(seq_state == MEM && irq_pending && !(ins_xfer && xfer_r2p)) irq_pending <= 0; else irq_pending <= irq_pending || irq; // else set irq_pending when irq is high diff --git a/tridoracpu/tridoracpu.srcs/top.v b/tridoracpu/tridoracpu.srcs/top.v index a4533d2..0dc3346 100644 --- a/tridoracpu/tridoracpu.srcs/top.v +++ b/tridoracpu/tridoracpu.srcs/top.v @@ -278,6 +278,9 @@ module top( (io_slot == 4) ? tdraudio_rd_data: `endif -1; + irqctrl irqctrl0(`clock, irq_in, irqc_cs, mem_write_enable, + irqc_seten, irqc_rd_data0, + irq); // CPU ----------------------------------------------------------------- stackcpu cpu0(.clk(`clock), .rst(rst), .irq(irq), @@ -288,10 +291,6 @@ module top( .mem_wait(mem_wait), .debug1(led1), .debug2(led2), .debug3(led3)); - // Interrupt Controller - irqctrl irqctrl0(`clock, irq_in, irqc_cs, mem_write_enable, - irqc_seten, irqc_rd_data0, - irq); // count clock ticks // generate interrupt every 20nth of a second diff --git a/tridoracpu/tridoracpu.xpr b/tridoracpu/tridoracpu.xpr index a9dc20f..a3dd3f6 100644 --- a/tridoracpu/tridoracpu.xpr +++ b/tridoracpu/tridoracpu.xpr @@ -358,9 +358,7 @@ - - Vivado Synthesis Defaults - + @@ -380,9 +378,7 @@ - - Default settings for Implementation. - + @@ -391,9 +387,7 @@ - - - + From 7751d8576520d55d6d6afeeb8d6f89f3c96a1828 Mon Sep 17 00:00:00 2001 From: slederer Date: Wed, 31 Dec 2025 13:24:20 +0100 Subject: [PATCH 09/29] pcomp: Makefile bugfixes --- pcomp/Makefile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pcomp/Makefile b/pcomp/Makefile index e183f73..4afc3ef 100644 --- a/pcomp/Makefile +++ b/pcomp/Makefile @@ -13,7 +13,7 @@ LSYMGEN=./lsymgen .pas: fpc -Mobjfpc -gl $< -all: pcomp sasm sdis lsymgen shortgen nativeprogs +all: libs pcomp sasm sdis lsymgen shortgen nativecomp nativeprogs libs: pcomp sasm lsymgen shortgen $(SASM) ../lib/coreloader.s @@ -22,9 +22,9 @@ libs: pcomp sasm lsymgen shortgen $(SASM) ../lib/stdlibwrap.s ../lib/stdlib.lib $(LSYMGEN) ../lib/stdlibwrap.sym ../lib/stdlib.lsym -test: sasm.s pcomp.s lsymgen.s shortgen.s +test: libs sasm.s pcomp.s lsymgen.s shortgen.s -testprgs: sasm.prog pcomp.prog lsymgen.prog shortgen.prog +testprgs: libs sasm.prog pcomp.prog lsymgen.prog shortgen.prog nativecomp: libs pcomp.prog sasm.prog lsymgen.prog shortgen.prog @@ -41,4 +41,5 @@ examples: nativecomp ../tests/readtest.prog ../tests/readchartest.prog ../tests/ -$(MAKE) -C ../rogue -f Makefile.tridoracpu clean: - rm -f pcomp sasm sdis libgen lsymgen *.o *.s *.prog + rm -f pcomp sasm sdis libgen lsymgen shortgen*.o *.s *.prog \ + ../lib/stdlib.s ../lib/stdlib.lib ../lib/stdlib.lsym From 79baf3cef534aa310f9f1d29da4cfaf5bd9ea16e Mon Sep 17 00:00:00 2001 From: slederer Date: Fri, 2 Jan 2026 22:49:54 +0100 Subject: [PATCH 10/29] serload: add exit command, correctly parse prompt after command --- utils/serload.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/utils/serload.py b/utils/serload.py index e69837f..0ee6962 100644 --- a/utils/serload.py +++ b/utils/serload.py @@ -78,6 +78,9 @@ def commandwait(ser, cmd): print("timeout sending '{}' command".format(cmd)) return None + if resp.startswith(b"> "): + resp = resp[2:] + if resp != bytearray(cmd + b"\r\n"): print("invalid response to '{}' command".format(cmd)) return None @@ -323,7 +326,7 @@ def showdata(ser): promptseen = True else: print(c.decode('utf8'), end='') - rest = ser.read(1) + rest = ser.read(1) # read trailing space of prompt def localdir(): @@ -360,8 +363,10 @@ def interactive(ser): print("superfluous argument") else: localdir() + elif cmd == 'exit' or cmd == 'x': + done = True else: - print("Unknown command. Valid commands are: dir get ldir put") + print("Unknown command. Valid commands are: dir get ldir put exit") if __name__ == "__main__": From 11814cd24fa147d278c6a648e7e36ee461e07e47 Mon Sep 17 00:00:00 2001 From: slederer Date: Fri, 2 Jan 2026 22:56:39 +0100 Subject: [PATCH 11/29] pcmaudio: bugfix corrupted audio, loop mode, adjust examples --- examples/pcmtest.pas | 53 +++++---- examples/pcmtest2.pas | 2 +- examples/xmas25.pas | 251 ++++++++++++++++++++++++++++++++++++++++++ lib/pcmaudio.inc | 2 +- lib/pcmaudio.s | 101 +++++++++++++---- 5 files changed, 363 insertions(+), 46 deletions(-) create mode 100644 examples/xmas25.pas diff --git a/examples/pcmtest.pas b/examples/pcmtest.pas index 423faaf..5122219 100644 --- a/examples/pcmtest.pas +++ b/examples/pcmtest.pas @@ -1,32 +1,21 @@ -{$H1536} -program pcmtest; +{$H2560} +program pcmtest2; uses pcmaudio; var filename:string; buf:SndBufPtr; - f:file; - size:integer; - i:integer; - c:char; sampleRate:integer; err:integer; + done:boolean; + c:char; + +function readAudioFile(fname:string):SndBufPtr; +var i,size:integer; + c:char; + buf:SndBufPtr; + f:file; begin - if ParamCount > 0 then - filename := ParamStr(1) - else - begin - write('Filename> '); - readln(filename); - end; - - err := 1; - if ParamCount > 1 then - val(ParamStr(2),sampleRate, err); - - if err <> 0 then - sampleRate := 16000; - - open(f, filename, ModeReadOnly); + open(f, fname, ModeReadOnly); size := FileSize(f); new(buf, size); @@ -41,6 +30,26 @@ begin close(f); + readAudioFile := buf; +end; + +begin + if ParamCount > 0 then + filename := ParamStr(1) + else + begin + write('Filename> '); + readln(filename); + end; + + err := 1; + if ParamCount > 1 then + val(ParamStr(2), sampleRate, err); + if err > 0 then + sampleRate := 22050; + + buf := readAudioFile(filename); + PlaySample(buf, sampleRate); dispose(buf); diff --git a/examples/pcmtest2.pas b/examples/pcmtest2.pas index f72e5e6..b5bf47a 100644 --- a/examples/pcmtest2.pas +++ b/examples/pcmtest2.pas @@ -50,7 +50,7 @@ begin buf := readAudioFile(filename); - SampleQStart(buf, sampleRate); + SampleQStart(buf, false, sampleRate); write('Press ESC to stop> '); done := false; diff --git a/examples/xmas25.pas b/examples/xmas25.pas new file mode 100644 index 0000000..1a7d8b5 --- /dev/null +++ b/examples/xmas25.pas @@ -0,0 +1,251 @@ +{$H2560} +{$S8} +program xmas252; +uses pcmaudio, fastfire, tiles; + +const MAXX = FIREWIDTH; + MAXY = FIREHEIGHT; + +(* type PixelData = array[0..31999] of integer; *) + +type Picture = record + magic:integer; + mode:integer; + palette: array[0..15] of integer; + pixels: PixelData; + end; + +var firecells: FireBuf; + + firepalette: array [0..15] of integer = + { ( $FFA, $FF8, $FF4, $FF0, $FE0, $FD0, $FA0, $F90, + $F00, $E00, $D00, $A00, $800, $600, $300, $000); } + { ( $FFA, $FFA, $FFA, $FFA, $FF0, $FF0, $FF0, $FF0, } + ( $00F, $00F, $00F, $00F, $00F, $00F, $00F, $00F, + $FF0, $FD0, $FA0, $C00, $A00, $700, $400, $000); + x,y:integer; + infile:file; + pic:^Picture; + tilesheet:^Picture; + animationTick:integer; + animationHold:integer; + animationState:integer; + + filename: string; + + audiodata: SndBufPtr; + +procedure createPalette; +var i:integer; +begin + for i := 15 downto 0 do + setpalette(15 - i, firepalette[i]); +end; + +procedure fireItUp; +var x,y:integer; +begin + y := MAXY - 1; + for x := 1 to MAXX - 1 do + firecells[y, x] := random and 127; +end; + + +procedure updateFire; +var i,x,y:integer; +begin + for y := 0 to MAXY - 2 do + for x := 1 to MAXX - 1 do + begin + i := + ((firecells[y + 1, x - 1] + + firecells[y + 1, x] + + firecells[y + 1, x + 1] + + firecells[y + 2, x]) + ) shr 2; + if i > 0 then + i := i - 1; + firecells[y, x] := i; + end; +end; + +procedure drawFire(startX,startY:integer); +var x, y, col, col2:integer; +begin + for y := 0 to MAXY - 1 do + begin + x := 0; + for col in firecells[y] do + begin + { scale and clamp color value } + col2 := col shr 3; + if col2 > FIREMAXCOLOR then col2 := FIREMAXCOLOR; + + putpixel(startX + x, startY + y, col2); + x := x + 1; + end; + end; +end; + +procedure readBackgroundPic(filename:string); +var i:integer; +begin + open(infile, filename, ModeReadonly); + read(infile, pic^); + close(infile); + + for i := 0 to 15 do + SetPalette(i, pic^.palette[i]); + + PutScreen(pic^.pixels); +end; + +procedure animate; +var tileSrcX,tilesrcY:integer; +begin + animationTick := animationTick + 1; + + if animationHold = 0 then + animationHold := 40; + + if animationTick < animationHold then + exit; + + animationTick := 0; + + case animationState of + 0: begin + tileSrcX := 0; + tileSrcY := 0; + animationHold := 40; + end; + 1: begin + tileSrcX := 19; + tileSrcY := 0; + animationHold := 20; + + if random and 7 > 4 then + animationState := -1; + end; + 2: begin + tileSrcX := 38; + tileSrcY := 0; + animationHold := 2; + end; + 3: begin; + tileSrcX := 57; + tileSrcY := 0; + animationHold := 2; + end; + 4: begin + tileSrcX := 0; + tileSrcY := 13; + animationHold := 15; + end; + 5: begin + tileSrcX := 57; + tileSrcY := 0; + animationHold := 2; + end; + 6: begin + tileSrcX := 38; + tileSrcY := 0; + animationHold := 2; + end; + 7: begin + tileSrcX := 0; + tileSrcY := 0; + animationHold := 2; + animationState := -1; + end; + end; + + CopyTilesScr(tilesheet^.pixels, + tileSrcX, tileSrcY, + 34,34, + 19,13); + + animationState := animationState + 1; +end; + + +procedure readTilesheet; +var filename:string; + i:integer; +begin + filename := 'tilesheet.pict'; + open(infile, filename, ModeReadonly); + read(infile, tilesheet^); + close(infile); +end; + +function newAudioData(fname:string):SndBufPtr; +var i,size:integer; + c:char; + buf:SndBufPtr; + f:file; +begin + open(f, fname, ModeReadOnly); + size := FileSize(f); + new(buf, size); + + buf^ := ''; + write('Reading ', size, ' bytes...'); + for i := 1 to size do + begin + read(f,c); + AppendChar(buf^,c); + end; + writeln; + + close(f); + + newAudioData := buf; +end; + + +begin + if ParamCount > 0 then + filename := ParamStr(1) + else + filename := 'xmas25bg.pict'; + + Randomize; + + audiodata := newAudioData('fireplace-loop.tdrau'); + + InitGraphics; + + new(pic); + readBackgroundPic(filename); + + new(tilesheet); + readTilesheet; + + SampleQStart(audiodata, true, 22050); + + while not ConAvail do + begin + fireItUp; + FastFireUpdate(firecells); + { updateFire; } + FastFireDraw(firecells, 216, 165); + { drawFire(216, 165); } + animate; + end; + + SampleQStop; + + for y := 0 to MAXY do + begin + x := firecells[y, 10]; + drawline(0, y, x, y, 1); + + end; + + InitGraphics; + + dispose(tilesheet); + dispose(pic); + dispose(audiodata); +end. diff --git a/lib/pcmaudio.inc b/lib/pcmaudio.inc index 4c3cdb3..dc1dbba 100644 --- a/lib/pcmaudio.inc +++ b/lib/pcmaudio.inc @@ -2,6 +2,6 @@ type SndBuf = string[32768]; type SndBufPtr = ^SndBuf; procedure PlaySample(buf:SndBufPtr;sampleRate:integer); external; -procedure SampleQStart(buf:SndBufPtr;sampleRate:integer); external; +procedure SampleQStart(buf:SndBufPtr;loop:boolean;sampleRate:integer); external; procedure SampleQStop; external; function SampleQSize:integer; external; diff --git a/lib/pcmaudio.s b/lib/pcmaudio.s index d1add4f..530f52f 100644 --- a/lib/pcmaudio.s +++ b/lib/pcmaudio.s @@ -1,25 +1,25 @@ .EQU AUDIO_BASE $A00 .EQU IRQC_REG $980 .EQU IRQC_EN $80 + .EQU CPU_FREQ 77000000 ; args: sample rate START_PCMAUDIO: ; calculate clock divider - LOADCP 77000000 + LOADCP CPU_FREQ SWAP LOADCP _DIV CALL LOADC AUDIO_BASE + 1 SWAP ; put clock divider on ToS -; LOADCP 4812 ; clock divider for 16KHz sample rate -; LOADCP 2406 ; clock divider for 32KHz sample rate STOREI 1 LOADCP 32768 ; set amplitude to biased 0 STOREI DROP + LOADC AUDIO_BASE - LOADC 17 ; enable channel, enable interrupt + LOADC 1 ; enable channel STOREI DROP RET @@ -101,18 +101,14 @@ PLAY1_L0: DROP RET -; start interrupt-driven sample playback -; args: pointer to pascal string, sample rate -SAMPLEQSTART: - LOADCP START_PCMAUDIO - CALL - +; set sample queue count and pointer from string header +; args: pointer to string/SndBufPtr +_STR2SMPLQPTR: LOADCP SMPLQ_COUNT OVER LOADI ; get string size from header SHR ; divide by 4 to get word count SHR - STOREI DROP @@ -121,6 +117,38 @@ SAMPLEQSTART: INC 8 ; skip rest of header STOREI ; store sample data pointer DROP + RET + +; start interrupt-driven sample playback +; args: pointer to pascal string, loop flag, sample rate +SAMPLEQSTART: + LOADCP START_PCMAUDIO ; sample rate is on ToS as arg to subroutine + CALL + + SWAP ; swap loop flag and buf ptr + + LOADCP _STR2SMPLQPTR + CALL + + ; loop flag is now on ToS + CBRANCH.Z SQ_S_1 + ; if nonzero, set loop ptr + LOADCP SMPLQ_PTR + LOADI + DEC 8 ; subtract offset for string header again + BRANCH SQ_S_0 +SQ_S_1: + LOADC 0 +SQ_S_0: + LOADCP SMPLQ_NEXT + SWAP + STOREI + DROP + + LOADC AUDIO_BASE + LOADC 17 ; enable channel, enable interrupt + STOREI + DROP LOADCP SMPLQ_ISR ; set interrupt handler STOREREG IV @@ -154,6 +182,7 @@ SAMPLEQSIZE: SMPLQ_PTR: .WORD 0 SMPLQ_COUNT: .WORD 0 +SMPLQ_NEXT: .WORD 0 SMPLQ_ISR: LOADC IRQC_REG @@ -170,7 +199,7 @@ SMPLQ_I_L: DROP BRANCH SMPLQ_I_XT ; if null, end interrupt routine SMPLQ_I_B: - LOADI ; load next word + LOADI ; load next word which contains two samples DUP BROT ; get high half-word @@ -205,23 +234,42 @@ SMPLQ_I_B: STOREI DROP - ; check if fifo is full - LOADC AUDIO_BASE - LOADI - LOADC 8 ; fifo_full + ; put up to 16 samples into the sample queue + LOADCP SMPLQ_COUNT + LOADI ; load word counter again + LOADC 7 ; check if count modulo 7 = 0 AND - CBRANCH.Z SMPLQ_I_L ; next sample if not full + CBRANCH.NZ SMPLQ_I_L ; if not, next two samples - LOADC AUDIO_BASE - LOADC 17 ; re-enable channel interrupt - STOREI + ; check if fifo is full + ; does not work reliably when running in DRAM, + ; maybe because at least one sample has already played + ; since start of ISR? +; LOADC AUDIO_BASE +; LOADI +; LOADC 8 ; fifo_full +; AND +; CBRANCH.Z SMPLQ_I_L ; next sample if not full + + BRANCH SMPLQ_I_XT + + ; end of sample buffer, check for next +SMPLQ_I_END: DROP + DROP + + LOADCP SMPLQ_NEXT ; skip to end + LOADI ; if NEXT ptr is zero + DUP + CBRANCH.Z SMPLQ_I_END1 + + LOADCP _STR2SMPLQPTR + CALL BRANCH SMPLQ_I_XT ; end playback, set ptr and counter to zero -SMPLQ_I_END: - DROP +SMPLQ_I_END1: DROP LOADCP SMPLQ_PTR LOADC 0 @@ -238,7 +286,16 @@ SMPLQ_I_END: STOREI DROP + ; exit without enabling interrupts for this channel + BRANCH SMPLQ_I_XT2 + SMPLQ_I_XT: + LOADC AUDIO_BASE + LOADC 17 ; re-enable channel interrupt + STOREI + DROP + +SMPLQ_I_XT2: LOADC IRQC_REG ; re-enable interrupts LOADC IRQC_EN STOREI From d17c4c41fd2b27fd94b2c28986626bb8f5a8387c Mon Sep 17 00:00:00 2001 From: slederer Date: Sun, 25 Jan 2026 23:23:22 +0100 Subject: [PATCH 12/29] docs: add section about units to the pascal programming guide --- doc/pascalprogramming.md | 79 ++++++++++++++++++++++++++++++++++++++++ lib/corelib.s | 2 +- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/doc/pascalprogramming.md b/doc/pascalprogramming.md index df5d3fd..454b46b 100644 --- a/doc/pascalprogramming.md +++ b/doc/pascalprogramming.md @@ -235,6 +235,85 @@ In Wirth Pascal, labels must be numbers. Other Pascal dialects also allow normal Tridora-Pascal only allows identifiers as labels. +## Units +Units are the method to create libraries in Tridora-Pascal, that is, codes module that can +be reused in other programs. + +Tridora-Pascal follows the unit syntax that has been established in UCSD-Pascal and is also +used in Turbo Pascal. + +Units are imported with the *USES* keyword, right after the *PROGRAM* statement. +Multiple units can be imported by separating the unit names with commas. + +There are some differences: In Tridora-Pascal, the unit file does not contain the interface +section, only the implementation section. The interface section is instead placed into a +separate file with the extension *.inc*, without any *UNIT* or *INTERFACE* keywords. + +This file will be included by the compiler and should contain +procedure or function declarations (as *EXTERNAL*). It can also contain *TYPE*, +*CONST* and *VAR* statements. + +All Pascal symbols of the unit are imported into the main program. There +is no separate namespace for units. + +### Using an Existing Unit +An existing unit is imported with the *USES* statement that must be placed +immediately after the *PROGRAM* statement. + +The compiler will look for an include file with the unit name and an *.inc* extension. +It will also +tell the assembler to include an assembly language file for each +unit. The filename must be the unit name plus an *.s* extension. + +Since there is no linker in Tridora-Pascal, all imported units will be +assembled together with the main program. + +The compiler looks for unit *.inc* and *.s* files in the current volume or +in the *SYSTEM* volume. + +### Compiling a Unit +A unit implementation file should start with a *UNIT* statement instead of a *PROGRAM* +statement. + +It should be compiled, not assembled. + +When building a program that uses units, the assembler will include an assembly language +file for each unit. + +It is possible to write units in assembly language. This is done by +directly providing the *.s* file and creating an *.inc* file with +the *EXTERNAL* declarations matching the assembly language +file. +#### Example +```pascal +(* UnitExamples.pas *) +program UnitExample; +uses hello; + +begin + sayHello('unit'); +end. +``` + +#### Example Unit Implementation File +```pascal +(* hello.pas *) +unit hello; + +implementation + +procedure sayHello(s:string); +begin + writeln('hello, ', s); +end; + +end. +``` +#### Example Unit Include File +```pascal +(* hello.inc *) +procedure sayHello(s:string); external; +``` ## Compiler Directives Tridora-Pascal understands a small number of compiler directives which are introduced as usual with a comment and a dollar-sign. Both comment styles can be used. diff --git a/lib/corelib.s b/lib/corelib.s index 8b8f403..d147934 100644 --- a/lib/corelib.s +++ b/lib/corelib.s @@ -822,7 +822,7 @@ PUTPIXEL_4BPP: SHL 2 ; * 16 DUP SHL 2; * 64 - ADD ; x*16 + x*64 + ADD ; y*16 + y*64 ADD ; add results together for vmem addr From 248c9ae919f9fe64adc294daa3baead6b35695c7 Mon Sep 17 00:00:00 2001 From: slederer Date: Mon, 26 Jan 2026 02:03:28 +0100 Subject: [PATCH 13/29] vgafb: first attempt at shifter/masker acceleration functionality --- tridoracpu/tridoracpu.srcs/top.v | 4 +- tridoracpu/tridoracpu.srcs/vgafb.v | 94 ++++++++++++++++++++++++++++++ tridoracpu/tridoracpu.xpr | 3 +- 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/tridoracpu/tridoracpu.srcs/top.v b/tridoracpu/tridoracpu.srcs/top.v index 0dc3346..bf3bea8 100644 --- a/tridoracpu/tridoracpu.srcs/top.v +++ b/tridoracpu/tridoracpu.srcs/top.v @@ -137,7 +137,7 @@ module top( assign fb_wr_data = mem_write_data; vgafb vgafb0(`clock, pixclk, rst, - mem_addr[3:0], fb_rd_data, fb_wr_data, + mem_addr[5:2], fb_rd_data, fb_wr_data, fb_rd_en, fb_wr_en, VGA_HS_O, VGA_VS_O, VGA_R, VGA_G, VGA_B); `endif @@ -247,7 +247,7 @@ module top( assign tdraudio_wr_data = mem_write_data; tdraudio tdraudio0(`clock, ~rst, - mem_addr[6:0], + mem_addr[8:2], tdraudio_rd_data, tdraudio_wr_data, tdraudio_rd_en, diff --git a/tridoracpu/tridoracpu.srcs/vgafb.v b/tridoracpu/tridoracpu.srcs/vgafb.v index f87e514..408079a 100644 --- a/tridoracpu/tridoracpu.srcs/vgafb.v +++ b/tridoracpu/tridoracpu.srcs/vgafb.v @@ -1,6 +1,9 @@ `timescale 1ns / 1ps `default_nettype none +// enable shifter/masker registers +`define ENABLE_FB_ACCEL + // Project F: Display Timings // (C)2019 Will Green, Open Source Hardware released under the MIT License // Learn more at https://projectf.io @@ -126,6 +129,14 @@ module vgafb #(VMEM_ADDR_WIDTH = 15, VMEM_DATA_WIDTH = 32) ( localparam REG_PAL_SLOT = 3; localparam REG_PAL_DATA = 4; localparam REG_CTL = 5; +`ifdef ENABLE_FB_ACCEL + localparam REG_SHIFTER = 6; + localparam REG_SHIFTCOUNT = 7; + localparam REG_SHIFTERM = 9; + localparam REG_SHIFTERSP = 10; + localparam REG_MASKGEN = 11; +`endif + localparam COLOR_WIDTH = 12; localparam PALETTE_WIDTH = 4; @@ -145,12 +156,30 @@ module vgafb #(VMEM_ADDR_WIDTH = 15, VMEM_DATA_WIDTH = 32) ( wire pix_rd; wire [VMEM_DATA_WIDTH-1:0] status; +`ifdef ENABLE_FB_ACCEL + reg [VMEM_DATA_WIDTH-1:0] acc_shifter_in; + reg [(VMEM_DATA_WIDTH*2)-1:0] acc_shifter_out; + reg [2:0] acc_shift_count; + reg acc_start_shift; + reg [VMEM_DATA_WIDTH-1:0] acc_mask_in; + wire [VMEM_DATA_WIDTH-1:0] acc_mask_out; + wire [VMEM_DATA_WIDTH-1:0] acc_shifter_mask; + wire [VMEM_DATA_WIDTH-1:0] acc_shifter_out_h = acc_shifter_out[(VMEM_DATA_WIDTH*2)-1:VMEM_DATA_WIDTH]; + wire [VMEM_DATA_WIDTH-1:0] acc_shifter_out_l = acc_shifter_out[VMEM_DATA_WIDTH-1:0]; + `endif + assign vmem_rd_en = rd_en; assign vmem_wr_en = (reg_sel == REG_VMEM) && wr_en; assign rd_data = (reg_sel == REG_VMEM) ? vmem_rd_data : (reg_sel == REG_RD_ADDR) ? cpu_rd_addr : (reg_sel == REG_WR_ADDR) ? cpu_wr_addr : (reg_sel == REG_CTL) ? status : +`ifdef ENABLE_FB_ACCEL + (reg_sel == REG_SHIFTER) ? acc_shifter_out_h: + (reg_sel == REG_SHIFTERM) ? acc_shifter_mask : + (reg_sel == REG_SHIFTERSP) ? acc_shifter_out_l : + (reg_sel == REG_MASKGEN) ? acc_mask_out : + `endif 32'hFFFFFFFF; wire [VMEM_ADDR_WIDTH-1:0] cpu_addr = vmem_wr_en ? cpu_wr_addr : cpu_rd_addr; @@ -271,6 +300,71 @@ module vgafb #(VMEM_ADDR_WIDTH = 15, VMEM_DATA_WIDTH = 32) ( if(rd_en && reg_sel == REG_VMEM) cpu_rd_addr <= cpu_rd_addr + 1; // auto-increment read addr on read end +`ifdef ENABLE_FB_ACCEL + // + // shifter/masker registers + // + always @(posedge cpu_clk) + begin + if(wr_en && reg_sel == REG_SHIFTER) + acc_shifter_in <= { wr_data, {32{1'b0}}}; + end + + always @(posedge cpu_clk) + begin + if(wr_en && reg_sel == REG_SHIFTCOUNT) + begin + acc_shift_count <= wr_data[2:0]; + acc_start_shift <= 1; + end + + if(acc_start_shift) + acc_start_shift <= 0; + end + + always @(posedge cpu_clk) + begin + if (acc_start_shift) + begin + acc_shifter_out <= {acc_shifter_in, {VMEM_DATA_WIDTH{1'b0}}} >> acc_shift_count; + end + end + + // mask register + always @(posedge cpu_clk) + begin + if (wr_en && reg_sel == REG_MASKGEN) + begin + acc_mask_in <= wr_data; + end + end + + assign acc_mask_out = { + {4{|{acc_mask_in[31:28]}}}, + {4{|{acc_mask_in[27:24]}}}, + {4{|{acc_mask_in[23:20]}}}, + {4{|{acc_mask_in[19:16]}}}, + {4{|{acc_mask_in[15:12]}}}, + {4{|{acc_mask_in[11:8]}}}, + {4{|{acc_mask_in[7:4]}}}, + {4{|{acc_mask_in[3:0]}}} + }; + + assign acc_shifter_mask = { + {4{|{acc_shifter_out_h[31:28]}}}, + {4{|{acc_shifter_out_h[27:24]}}}, + {4{|{acc_shifter_out_h[23:20]}}}, + {4{|{acc_shifter_out_h[19:16]}}}, + {4{|{acc_shifter_out_h[15:12]}}}, + {4{|{acc_shifter_out_h[11:8]}}}, + {4{|{acc_shifter_out_h[7:4]}}}, + {4{|{acc_shifter_out_h[3:0]}}} + }; +`endif + + // + // shifting pixels at pixel clock + // always @(posedge pix_clk) begin if(scanline || shift_count == MAX_SHIFT_COUNT) // before start of a line diff --git a/tridoracpu/tridoracpu.xpr b/tridoracpu/tridoracpu.xpr index a3dd3f6..2926f59 100644 --- a/tridoracpu/tridoracpu.xpr +++ b/tridoracpu/tridoracpu.xpr @@ -376,7 +376,7 @@ - + @@ -389,7 +389,6 @@ - From 937369f60b5349fe7d8f71b6f6f4b3d4190e3e9f Mon Sep 17 00:00:00 2001 From: slederer Date: Wed, 28 Jan 2026 01:15:16 +0100 Subject: [PATCH 14/29] lib,examples: changes for new register address mapping --- examples/fastfire.s | 10 +++++----- examples/sprites.s | 10 +++++----- lib/corelib.s | 10 +++++----- lib/pcmaudio.s | 10 +++++----- tridoracpu/tridoracpu.srcs/vgafb.v | 10 +++------- tridoracpu/tridoracpu.xpr | 3 ++- 6 files changed, 25 insertions(+), 28 deletions(-) diff --git a/examples/fastfire.s b/examples/fastfire.s index f0e10e4..63ace51 100644 --- a/examples/fastfire.s +++ b/examples/fastfire.s @@ -123,11 +123,11 @@ FF_EXIT: ; framebuffer controller registers .EQU FB_RA $900 - .EQU FB_WA $901 - .EQU FB_IO $902 - .EQU FB_PS $903 - .EQU FB_PD $904 - .EQU FB_CTL $905 + .EQU FB_WA $904 + .EQU FB_IO $908 + .EQU FB_PS $90C + .EQU FB_PD $910 + .EQU FB_CTL $914 .EQU WORDS_PER_LINE 80 ; fire width in vmem words (strict left-to-right evaluation) diff --git a/examples/sprites.s b/examples/sprites.s index 3391339..6962eda 100644 --- a/examples/sprites.s +++ b/examples/sprites.s @@ -3,9 +3,9 @@ .EQU WORDS_PER_LINE 80 .EQU FB_RA $900 - .EQU FB_WA $901 - .EQU FB_IO $902 - .EQU FB_PS $903 + .EQU FB_WA $904 + .EQU FB_IO $908 + .EQU FB_PS $90C ; calculate mask for a word of pixels ; args: word of pixels with four bits per pixel @@ -95,7 +95,7 @@ PS_LOOP1: ; in the vga controller LOADC FB_RA ; read address register LOAD PS_VMEM_ADDR - STOREI 1 ; use autoincrement to get to the next register + STOREI 4 ; use autoincrement to get to the next register LOAD PS_VMEM_ADDR STOREI DROP @@ -322,7 +322,7 @@ UD_S_L1: ; store vmem offset into write addr reg LOADCP FB_WA LOAD UD_S_OFFSET - STOREI 1 ; ugly but fast: reuse addr + STOREI 4 ; ugly but fast: reuse addr ; with postincrement to ; get to FB_IO for STOREI below diff --git a/lib/corelib.s b/lib/corelib.s index d147934..a21b95c 100644 --- a/lib/corelib.s +++ b/lib/corelib.s @@ -701,11 +701,11 @@ CMPWORDS_XT2: ; --------- Graphics Library --------------- ; vga controller registers .EQU FB_RA $900 - .EQU FB_WA $901 - .EQU FB_IO $902 - .EQU FB_PS $903 - .EQU FB_PD $904 - .EQU FB_CTL $905 + .EQU FB_WA $904 + .EQU FB_IO $908 + .EQU FB_PS $90C + .EQU FB_PD $910 + .EQU FB_CTL $914 ; set a pixel in fb memory ; parameters: x,y - coordinates PUTPIXEL_1BPP: diff --git a/lib/pcmaudio.s b/lib/pcmaudio.s index 530f52f..ebe812a 100644 --- a/lib/pcmaudio.s +++ b/lib/pcmaudio.s @@ -11,9 +11,9 @@ START_PCMAUDIO: LOADCP _DIV CALL - LOADC AUDIO_BASE + 1 + LOADC AUDIO_BASE + 4 SWAP ; put clock divider on ToS - STOREI 1 + STOREI 4 LOADCP 32768 ; set amplitude to biased 0 STOREI DROP @@ -95,7 +95,7 @@ PLAY1_L0: AND CBRANCH.NZ PLAY1_L0 ; loop if fifo is full - LOADC AUDIO_BASE+2 ; store amplitude value + LOADC AUDIO_BASE+8 ; store amplitude value SWAP STOREI DROP @@ -207,7 +207,7 @@ SMPLQ_I_B: LOADCP $FFFF AND - LOADC AUDIO_BASE+2 + LOADC AUDIO_BASE+8 SWAP STOREI ; write sample, keep addr @@ -281,7 +281,7 @@ SMPLQ_I_END1: DROP ; set amplitude out to zero (biased) - LOADC AUDIO_BASE+2 + LOADC AUDIO_BASE+8 LOADCP 32768 STOREI DROP diff --git a/tridoracpu/tridoracpu.srcs/vgafb.v b/tridoracpu/tridoracpu.srcs/vgafb.v index 408079a..2d6bc55 100644 --- a/tridoracpu/tridoracpu.srcs/vgafb.v +++ b/tridoracpu/tridoracpu.srcs/vgafb.v @@ -132,9 +132,9 @@ module vgafb #(VMEM_ADDR_WIDTH = 15, VMEM_DATA_WIDTH = 32) ( `ifdef ENABLE_FB_ACCEL localparam REG_SHIFTER = 6; localparam REG_SHIFTCOUNT = 7; - localparam REG_SHIFTERM = 9; - localparam REG_SHIFTERSP = 10; - localparam REG_MASKGEN = 11; + localparam REG_SHIFTERM = 8; + localparam REG_SHIFTERSP = 09; + localparam REG_MASKGEN = 10; `endif localparam COLOR_WIDTH = 12; @@ -325,18 +325,14 @@ module vgafb #(VMEM_ADDR_WIDTH = 15, VMEM_DATA_WIDTH = 32) ( always @(posedge cpu_clk) begin if (acc_start_shift) - begin acc_shifter_out <= {acc_shifter_in, {VMEM_DATA_WIDTH{1'b0}}} >> acc_shift_count; - end end // mask register always @(posedge cpu_clk) begin if (wr_en && reg_sel == REG_MASKGEN) - begin acc_mask_in <= wr_data; - end end assign acc_mask_out = { diff --git a/tridoracpu/tridoracpu.xpr b/tridoracpu/tridoracpu.xpr index 2926f59..a3dd3f6 100644 --- a/tridoracpu/tridoracpu.xpr +++ b/tridoracpu/tridoracpu.xpr @@ -376,7 +376,7 @@ - + @@ -389,6 +389,7 @@ + From 042a18fc9b7ecd5b93b88a1f1a3ea4b633302b4b Mon Sep 17 00:00:00 2001 From: slederer Date: Thu, 29 Jan 2026 01:53:35 +0100 Subject: [PATCH 15/29] vgafb: bugfixes, change synthesis optimization settings --- tridoracpu/tridoracpu.srcs/vgafb.v | 6 +++--- tridoracpu/tridoracpu.xpr | 34 ++++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/tridoracpu/tridoracpu.srcs/vgafb.v b/tridoracpu/tridoracpu.srcs/vgafb.v index 2d6bc55..411e956 100644 --- a/tridoracpu/tridoracpu.srcs/vgafb.v +++ b/tridoracpu/tridoracpu.srcs/vgafb.v @@ -159,7 +159,7 @@ module vgafb #(VMEM_ADDR_WIDTH = 15, VMEM_DATA_WIDTH = 32) ( `ifdef ENABLE_FB_ACCEL reg [VMEM_DATA_WIDTH-1:0] acc_shifter_in; reg [(VMEM_DATA_WIDTH*2)-1:0] acc_shifter_out; - reg [2:0] acc_shift_count; + reg [4:0] acc_shift_count; reg acc_start_shift; reg [VMEM_DATA_WIDTH-1:0] acc_mask_in; wire [VMEM_DATA_WIDTH-1:0] acc_mask_out; @@ -307,14 +307,14 @@ module vgafb #(VMEM_ADDR_WIDTH = 15, VMEM_DATA_WIDTH = 32) ( always @(posedge cpu_clk) begin if(wr_en && reg_sel == REG_SHIFTER) - acc_shifter_in <= { wr_data, {32{1'b0}}}; + acc_shifter_in <= wr_data; end always @(posedge cpu_clk) begin if(wr_en && reg_sel == REG_SHIFTCOUNT) begin - acc_shift_count <= wr_data[2:0]; + acc_shift_count <= { wr_data[2:0], 2'b0}; acc_start_shift <= 1; end diff --git a/tridoracpu/tridoracpu.xpr b/tridoracpu/tridoracpu.xpr index a3dd3f6..4d21f83 100644 --- a/tridoracpu/tridoracpu.xpr +++ b/tridoracpu/tridoracpu.xpr @@ -356,10 +356,16 @@ - + - - + + Performs optimizations which creates alternative logic technology mapping, including disabling LUT combining, forcing F7/F8/F9 to logic, increasing the threshold of shift register inference. + + + + + + @@ -376,16 +382,26 @@ - + - + + Best predicted directive for place_design. + - + + + - + + + - - + + + + + + From 8900eb90be47d2d3eceb5b2e2b5417ce58b24993 Mon Sep 17 00:00:00 2001 From: slederer Date: Sat, 31 Jan 2026 02:31:00 +0100 Subject: [PATCH 16/29] corelib: new putpixel routine using shifter/maskgen --- lib/corelib.s | 190 +++++++++----------------------------------------- 1 file changed, 34 insertions(+), 156 deletions(-) diff --git a/lib/corelib.s b/lib/corelib.s index a21b95c..b228d20 100644 --- a/lib/corelib.s +++ b/lib/corelib.s @@ -706,108 +706,32 @@ CMPWORDS_XT2: .EQU FB_PS $90C .EQU FB_PD $910 .EQU FB_CTL $914 -; set a pixel in fb memory -; parameters: x,y - coordinates -PUTPIXEL_1BPP: - ; calculate vmem address: - OVER ; duplicate x - ; divide x by 32 - SHR - SHR - SHR - SHR - SHR - SWAP - ; multiply y by words per line - SHL 2 - SHL 2 - SHL + .EQU FB_SHIFTER $918 + .EQU FB_SHIFTCOUNT $91C + .EQU FB_SHIFTERM $920 + .EQU FB_SHIFTERSP $924 + .EQU FB_MASKGEN $928 - ADD ; add results together for vmem addr +; draw a single pixel +; args: x, y, color - DUP - LOADCP FB_WA - SWAP - STOREI ; store to framebuffer write addr register - DROP - LOADCP FB_RA ; and to framebuffer read addr register - SWAP - STOREI - DROP - - ; x is now at top of stack - ; get bit value from x modulo 32 - LOADC 31 - AND - SHL 2 ; (x & 31) * 4 = offset into table - LOADCP INT_TO_PIX_TABLE - ADD - LOADI - - LOADCP FB_IO - ; read old vmem value - LOADCP FB_IO - LOADI - ; or in new bit - OR - ; write new value - STOREI - DROP - - RET - -INT_TO_PIX_TABLE: - .WORD %10000000_00000000_00000000_00000000 - .WORD %01000000_00000000_00000000_00000000 - .WORD %00100000_00000000_00000000_00000000 - .WORD %00010000_00000000_00000000_00000000 - .WORD %00001000_00000000_00000000_00000000 - .WORD %00000100_00000000_00000000_00000000 - .WORD %00000010_00000000_00000000_00000000 - .WORD %00000001_00000000_00000000_00000000 - .WORD %00000000_10000000_00000000_00000000 - .WORD %00000000_01000000_00000000_00000000 - .WORD %00000000_00100000_00000000_00000000 - .WORD %00000000_00010000_00000000_00000000 - .WORD %00000000_00001000_00000000_00000000 - .WORD %00000000_00000100_00000000_00000000 - .WORD %00000000_00000010_00000000_00000000 - .WORD %00000000_00000001_00000000_00000000 - .WORD %00000000_00000000_10000000_00000000 - .WORD %00000000_00000000_01000000_00000000 - .WORD %00000000_00000000_00100000_00000000 - .WORD %00000000_00000000_00010000_00000000 - .WORD %00000000_00000000_00001000_00000000 - .WORD %00000000_00000000_00000100_00000000 - .WORD %00000000_00000000_00000010_00000000 - .WORD %00000000_00000000_00000001_00000000 - .WORD %00000000_00000000_00000000_10000000 - .WORD %00000000_00000000_00000000_01000000 - .WORD %00000000_00000000_00000000_00100000 - .WORD %00000000_00000000_00000000_00010000 - .WORD %00000000_00000000_00000000_00001000 - .WORD %00000000_00000000_00000000_00000100 - .WORD %00000000_00000000_00000000_00000010 - .WORD %00000000_00000000_00000000_00000001 - -PUTMPIXEL: - LOADC 1 -; set a pixel in fb memory -; parameters: x,y,color - coordinates, color value (0-15) PUTPIXEL: PUTPIXEL_4BPP: .EQU PUTPIXEL_X 0 .EQU PUTPIXEL_Y 4 .EQU PUTPIXEL_COLOR 8 - .EQU PUTPIXEL_PIXPOS 12 + .EQU PUTPIXEL_BPSAV 12 .EQU PUTPIXEL_FS 16 FPADJ -PUTPIXEL_FS - STORE PUTPIXEL_COLOR STORE PUTPIXEL_Y STORE PUTPIXEL_X + LOADREG BP + STORE PUTPIXEL_BPSAV + LOADC 0 + STOREREG BP ; calculate vmem address: (x / 8) + (y * 80) LOAD PUTPIXEL_X @@ -826,83 +750,37 @@ PUTPIXEL_4BPP: ADD ; add results together for vmem addr - LOADCP FB_WA - OVER - STOREI ; store to framebuffer write addr register - DROP - LOADCP FB_RA ; and to framebuffer read addr register - SWAP ; swap addr and value for STOREI - STOREI - DROP - - LOAD PUTPIXEL_X - ; |0000.0000|0000.0000|0000.0000|0000.1111| - LOADC 7 - AND ; calculate pixel position in word - LOADC 7 - SWAP - SUB ; pixpos = 7 - (x & 7) - STORE PUTPIXEL_PIXPOS + DUP + STORE.B FB_WA ; set as write and read addresses + STORE.B FB_RA + ; create pixel data from color value in + ; leftmost pixel data bits (31-28) LOAD PUTPIXEL_COLOR - LOAD PUTPIXEL_PIXPOS - SHR ; rcount = pixpos / 2 -ROTLOOP_: - DUP ; exit loop if rcount is 0 - CBRANCH.Z ROTLOOP_END - SWAP ; pixel value is now on top of stack - BROT ; value = value << 8 - SWAP ; rcount is now on top of stack - DEC 1 ; rcount = rcount - 1 - BRANCH ROTLOOP_ -ROTLOOP_END: - DROP ; drop rcount - ; shifted pixel value is now at top of stack - LOAD PUTPIXEL_PIXPOS - LOADC 1 - AND - CBRANCH.Z EVEN_PIXPOS - SHL 2 ; if pixpos is odd, shift by 4 bits + BROT + BROT + BROT SHL 2 -EVEN_PIXPOS: - LOAD PUTPIXEL_X - ; get bit value from x modulo 8 - LOADC 7 - AND - SHL 2 ; (x & 7) * 4 = offset into table - LOADCP INT_TO_MASK_TABLE - ADD - LOADI + SHL 2 + STORE.B FB_SHIFTER ; store pixel into shifter - ; read old vmem value - LOADCP FB_IO - LOADI - ; mask bits - AND - ; or in shifted pixel value - OR + LOAD PUTPIXEL_X ; use x coord as shift count + STORE.B FB_SHIFTCOUNT ; writing triggers shifting - ; write new value - LOADCP FB_IO - SWAP - STOREI - DROP + LOAD.B FB_SHIFTERM ; get shift result as mask + LOAD.B FB_IO ; get background pixel data + AND ; remove bits for new pixel from bg + + LOAD.B FB_SHIFTER ; load shifted pixel + OR ; OR in new pixel bits + STORE.B FB_IO ; write new pixel data word to vmem + + LOAD PUTPIXEL_BPSAV + STOREREG BP FPADJ PUTPIXEL_FS RET - .CPOOL - -INT_TO_MASK_TABLE: - .WORD %00001111_11111111_11111111_11111111 - .WORD %11110000_11111111_11111111_11111111 - .WORD %11111111_00001111_11111111_11111111 - .WORD %11111111_11110000_11111111_11111111 - .WORD %11111111_11111111_00001111_11111111 - .WORD %11111111_11111111_11110000_11111111 - .WORD %11111111_11111111_11111111_00001111 - .WORD %11111111_11111111_11111111_11110000 - ; draw a line between two points ; parameters: x0, y0, x1, y1, color .EQU DL_X0 0 From 1e56251fc1417ff53f2444c578e4a3323400c0c9 Mon Sep 17 00:00:00 2001 From: slederer Date: Sat, 31 Jan 2026 17:24:36 +0100 Subject: [PATCH 17/29] vgafb: buffer maskgen outputs to avoid timing problems --- lib/corelib.s | 5 ++- tridoracpu/tridoracpu.srcs/vgafb.v | 57 +++++++++++++++++------------- tridoracpu/tridoracpu.xpr | 8 ++--- 3 files changed, 37 insertions(+), 33 deletions(-) diff --git a/lib/corelib.s b/lib/corelib.s index b228d20..93dc81f 100644 --- a/lib/corelib.s +++ b/lib/corelib.s @@ -756,10 +756,9 @@ PUTPIXEL_4BPP: ; create pixel data from color value in ; leftmost pixel data bits (31-28) + LOADC 0 LOAD PUTPIXEL_COLOR - BROT - BROT - BROT + BPLC SHL 2 SHL 2 STORE.B FB_SHIFTER ; store pixel into shifter diff --git a/tridoracpu/tridoracpu.srcs/vgafb.v b/tridoracpu/tridoracpu.srcs/vgafb.v index 411e956..fd42627 100644 --- a/tridoracpu/tridoracpu.srcs/vgafb.v +++ b/tridoracpu/tridoracpu.srcs/vgafb.v @@ -162,10 +162,12 @@ module vgafb #(VMEM_ADDR_WIDTH = 15, VMEM_DATA_WIDTH = 32) ( reg [4:0] acc_shift_count; reg acc_start_shift; reg [VMEM_DATA_WIDTH-1:0] acc_mask_in; - wire [VMEM_DATA_WIDTH-1:0] acc_mask_out; - wire [VMEM_DATA_WIDTH-1:0] acc_shifter_mask; + reg [VMEM_DATA_WIDTH-1:0] acc_mask_buf; + reg [VMEM_DATA_WIDTH-1:0] acc_shiftmask_buf; + wire [VMEM_DATA_WIDTH-1:0] acc_shifter_mask = acc_shiftmask_buf; wire [VMEM_DATA_WIDTH-1:0] acc_shifter_out_h = acc_shifter_out[(VMEM_DATA_WIDTH*2)-1:VMEM_DATA_WIDTH]; wire [VMEM_DATA_WIDTH-1:0] acc_shifter_out_l = acc_shifter_out[VMEM_DATA_WIDTH-1:0]; + `endif assign vmem_rd_en = rd_en; @@ -176,9 +178,9 @@ module vgafb #(VMEM_ADDR_WIDTH = 15, VMEM_DATA_WIDTH = 32) ( (reg_sel == REG_CTL) ? status : `ifdef ENABLE_FB_ACCEL (reg_sel == REG_SHIFTER) ? acc_shifter_out_h: - (reg_sel == REG_SHIFTERM) ? acc_shifter_mask : + (reg_sel == REG_SHIFTERM) ? acc_shiftmask_buf : (reg_sel == REG_SHIFTERSP) ? acc_shifter_out_l : - (reg_sel == REG_MASKGEN) ? acc_mask_out : + (reg_sel == REG_MASKGEN) ? acc_mask_buf : `endif 32'hFFFFFFFF; @@ -335,27 +337,34 @@ module vgafb #(VMEM_ADDR_WIDTH = 15, VMEM_DATA_WIDTH = 32) ( acc_mask_in <= wr_data; end - assign acc_mask_out = { - {4{|{acc_mask_in[31:28]}}}, - {4{|{acc_mask_in[27:24]}}}, - {4{|{acc_mask_in[23:20]}}}, - {4{|{acc_mask_in[19:16]}}}, - {4{|{acc_mask_in[15:12]}}}, - {4{|{acc_mask_in[11:8]}}}, - {4{|{acc_mask_in[7:4]}}}, - {4{|{acc_mask_in[3:0]}}} - }; + // mask output is buffered to avoid timing problems + always @(posedge cpu_clk) + begin + acc_mask_buf <= { + {4{~|{acc_mask_in[31:28]}}}, + {4{~|{acc_mask_in[27:24]}}}, + {4{~|{acc_mask_in[23:20]}}}, + {4{~|{acc_mask_in[19:16]}}}, + {4{~|{acc_mask_in[15:12]}}}, + {4{~|{acc_mask_in[11:8]}}}, + {4{~|{acc_mask_in[7:4]}}}, + {4{~|{acc_mask_in[3:0]}}} + }; + end - assign acc_shifter_mask = { - {4{|{acc_shifter_out_h[31:28]}}}, - {4{|{acc_shifter_out_h[27:24]}}}, - {4{|{acc_shifter_out_h[23:20]}}}, - {4{|{acc_shifter_out_h[19:16]}}}, - {4{|{acc_shifter_out_h[15:12]}}}, - {4{|{acc_shifter_out_h[11:8]}}}, - {4{|{acc_shifter_out_h[7:4]}}}, - {4{|{acc_shifter_out_h[3:0]}}} - }; + always @(posedge cpu_clk) + begin + acc_shiftmask_buf = { + {4{~|{acc_shifter_out_h[31:28]}}}, + {4{~|{acc_shifter_out_h[27:24]}}}, + {4{~|{acc_shifter_out_h[23:20]}}}, + {4{~|{acc_shifter_out_h[19:16]}}}, + {4{~|{acc_shifter_out_h[15:12]}}}, + {4{~|{acc_shifter_out_h[11:8]}}}, + {4{~|{acc_shifter_out_h[7:4]}}}, + {4{~|{acc_shifter_out_h[3:0]}}} + }; + end `endif // diff --git a/tridoracpu/tridoracpu.xpr b/tridoracpu/tridoracpu.xpr index 4d21f83..a088319 100644 --- a/tridoracpu/tridoracpu.xpr +++ b/tridoracpu/tridoracpu.xpr @@ -358,9 +358,7 @@ - - Performs optimizations which creates alternative logic technology mapping, including disabling LUT combining, forcing F7/F8/F9 to logic, increasing the threshold of shift register inference. - + @@ -384,9 +382,7 @@ - - Best predicted directive for place_design. - + From c119a2a5bb25a12f6fc13ca1b9f0f43c9ba8703e Mon Sep 17 00:00:00 2001 From: slederer Date: Sat, 31 Jan 2026 17:26:13 +0100 Subject: [PATCH 18/29] add line/points drawing benchmark --- examples/graphbench.pas | 92 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 examples/graphbench.pas diff --git a/examples/graphbench.pas b/examples/graphbench.pas new file mode 100644 index 0000000..327e72e --- /dev/null +++ b/examples/graphbench.pas @@ -0,0 +1,92 @@ +program graphbench; +var starttime,endtime:DateTime; + +procedure startBench(name:string); +begin + write(name:20, ' '); + starttime := GetTime; +end; + +procedure endBench; +var secDelta, minDelta, hourDelta:integer; + procedure write2Digits(i:integer); + begin + if i < 10 then + write('0'); + write(i); + end; +begin + endTime := GetTime; + + hourDelta := endtime.hours - starttime.hours; + minDelta := endtime.minutes - starttime.minutes; + secDelta := endtime.seconds - starttime.seconds; + + if secDelta < 0 then + begin + secDelta := 60 + secDelta; + minDelta := minDelta - 1; + end; + + if minDelta < 0 then + begin + minDelta := 60 + minDelta; + hourDelta := hourDelta - 1; + end; + + write2Digits(hourDelta); + write(':'); write2Digits(minDelta); + write(':'); write2Digits(secDelta); + writeln; +end; + +function randint(lessthan:integer):integer; +var r:integer; +begin + r := random and 511; + if r >= lessthan then + r := r - lessthan; + randint := r; +end; + +procedure drawlines(count:integer); +var i,col,x1,y1,x2,y2:integer; +begin + col := 1; + for i := 1 to count do + begin + x1 := randint(500); + y1 := randint(400); + x2 := randint(500); + y2 := randint(400); + DrawLine(x1,y1,x2,y2,col); + col := col + 1; + if col > 15 then col := 1; + end; +end; + +procedure drawpoints(count:integer); +var i,col,x,y:integer; +begin + col := 1; + for i := 1 to count do + begin + x := randint(500); + y := randint(400); + PutPixel(x,y,col); + col := col + 1; + if col > 15 then col := 1; + end; +end; + +begin + InitGraphics; + startBench('200K points'); + drawpoints(200000); + endBench; + + InitGraphics; + startBench('10K lines'); + drawlines(10000); + endBench; +end. From 66a50d5ea86bb28891476fd41b485480cfefac31 Mon Sep 17 00:00:00 2001 From: slederer Date: Sun, 1 Feb 2026 00:44:34 +0100 Subject: [PATCH 19/29] update sprites unit to use shifter/maskgen --- examples/graphbench.pas | 39 +++++++- examples/sprites.s | 204 ++++++++++------------------------------ 2 files changed, 86 insertions(+), 157 deletions(-) diff --git a/examples/graphbench.pas b/examples/graphbench.pas index 327e72e..9abbfba 100644 --- a/examples/graphbench.pas +++ b/examples/graphbench.pas @@ -1,5 +1,17 @@ program graphbench; +uses sprites; + var starttime,endtime:DateTime; + spriteData:SpritePixels; + +procedure readSpriteData(filename:string); +var f:file; +begin + open(f,filename,ModeReadOnly); + seek(f,8); (* skip file header *) + read(f,spriteData); + close(f); +end; procedure startBench(name:string); begin @@ -13,7 +25,7 @@ var secDelta, minDelta, hourDelta:integer; begin if i < 10 then write('0'); - write(i); + write(i); end; begin endTime := GetTime; @@ -49,6 +61,20 @@ begin randint := r; end; +procedure drawsprites(count:integer); +var i,col,x,y:integer; +begin + col := 1; + for i := 1 to count do + begin + x := randint(350); + y := randint(350); + PutSprite(x,y,spriteData); + col := col + 1; + if col > 15 then col := 1; + end; +end; + procedure drawlines(count:integer); var i,col,x1,y1,x2,y2:integer; begin @@ -80,13 +106,20 @@ begin end; begin + readSpriteData('rocket.sprt'); + InitGraphics; - startBench('200K points'); + startBench('points 200K'); drawpoints(200000); endBench; InitGraphics; - startBench('10K lines'); + startBench('lines 10K'); drawlines(10000); endBench; + + InitGraphics; + startBench('sprites 50K'); + drawsprites(50000); + endBench; end. diff --git a/examples/sprites.s b/examples/sprites.s index 6962eda..ab2e580 100644 --- a/examples/sprites.s +++ b/examples/sprites.s @@ -6,28 +6,13 @@ .EQU FB_WA $904 .EQU FB_IO $908 .EQU FB_PS $90C - -; 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 + .EQU FB_PD $910 + .EQU FB_CTL $914 + .EQU FB_SHIFTER $918 + .EQU FB_SHIFTCOUNT $91C + .EQU FB_SHIFTERM $920 + .EQU FB_SHIFTERSP $924 + .EQU FB_MASKGEN $928 ; calculate vmem address from coordinates ; args: x,y @@ -67,13 +52,19 @@ CALC_VMEM_ADDR: .EQU PS_SHIFT_C 20 .EQU PS_SPILL 24 .EQU PS_STRIPE_C 28 - .EQU PS_FS 32 + .EQU PS_BPSAVE 32 + .EQU PS_FS 36 PUTSPRITE: FPADJ -PS_FS STORE PS_SPRITE_DATA STORE PS_Y STORE PS_X + LOADREG BP + STORE PS_BPSAVE + LOADC 0 + STOREREG BP + ; calculate vmem address LOAD PS_X LOAD PS_Y @@ -81,11 +72,6 @@ PUTSPRITE: 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 @@ -93,12 +79,10 @@ PUTSPRITE: PS_LOOP1: ; set read and write address ; in the vga controller - LOADC FB_RA ; read address register LOAD PS_VMEM_ADDR - STOREI 4 ; use autoincrement to get to the next register - LOAD PS_VMEM_ADDR - STOREI - DROP + DUP + STORE.B FB_RA + STORE.B FB_WA LOAD PS_SPRITE_DATA ; address of sprite data DUP @@ -106,61 +90,19 @@ PS_LOOP1: STORE PS_SPRITE_DATA ; and store it again LOADI ; load word from orig. address + ; ------- one word of sprite pixels on stack - LOADC 0 - STORE PS_SPILL + STORE.B FB_SHIFTER + LOAD PS_X + STORE.B FB_SHIFTCOUNT - ; 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 + LOAD.B FB_SHIFTERM ; get shifted mask + LOAD.B FB_IO ; and background pixel data + AND ; remove foreground pixels - 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 + LOAD.B FB_SHIFTER ; get shifted pixels + OR ; combine with background + STORE.B FB_IO ; store into vmem ; set counter for remaining stripes LOADC SPRITE_STRIPES - 1 @@ -170,8 +112,8 @@ PS_LOOP2_X: ; process spilled bits and next vertical stripe of sprite data ; PS_NEXT_STRIPE: - ; put spill bits on stack for later - LOAD PS_SPILL + ;use spill bits from first column + LOAD.B FB_SHIFTERSP LOAD PS_SPRITE_DATA ; address of sprite data DUP @@ -179,65 +121,20 @@ PS_NEXT_STRIPE: STORE PS_SPRITE_DATA ; and store it again LOADI ; load word from orig. address - ; reset spill bits - LOADC 0 - STORE PS_SPILL - - ; last spill bits are on ToS now - - ; shift pixel data to right - LOAD PS_SHIFT_C ; load shift count -PS_LOOP3: ; test it for zero + STORE.B FB_SHIFTER ; store into shifter + LOAD PS_X + STORE.B FB_SHIFTCOUNT ; shift stuff + LOAD.B FB_SHIFTER ; get shifted pixels + OR ; combine with spill bits (see above) DUP - CBRANCH.Z PS_LOOP3_X + STORE.B FB_MASKGEN ; store to mask reg to get new mask - SWAP ; swap count with pixels + LOAD.B FB_MASKGEN ; get mask for spill bits + shifted pixels + LOAD.B FB_IO ; get vmem data + AND ; remove foreground pixels from bg - ; 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 + OR ; combine with shifted pixels + STORE.B FB_IO ; write to vmem LOAD PS_STRIPE_C ; decrement stripe count DEC 1 @@ -246,22 +143,18 @@ PS_LOOP3_X: CBRANCH.NZ PS_NEXT_STRIPE ; if non-zero, next stripe ; write spilled bits of the last stripe into next vmem word - LOAD PS_SPILL ; get spill bits + LOAD.B FB_SHIFTERSP ; get spill bits DUP - LOADCP CALC_MASK ; calculate sprite mask for spill bits - CALL + STORE.B FB_MASKGEN + LOAD.B FB_MASKGEN ; get sprite mask for spill bits - LOADCP FB_IO - LOADI ; load next vmem word + LOAD.B FB_IO ; 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 - + STORE.B FB_IO ; write to vmem + LOAD PS_SPRITE_LINES ; decrement lines count DEC 1 DUP @@ -275,7 +168,10 @@ PS_LOOP3_X: BRANCH PS_LOOP1 PS_L_XT: DROP - + + LOAD PS_BPSAVE + STOREREG BP + FPADJ PS_FS RET From bf813fac1d43250d26eac49c1a2847507fee2919 Mon Sep 17 00:00:00 2001 From: slederer Date: Sun, 1 Feb 2026 11:52:16 +0100 Subject: [PATCH 20/29] corelib: revert PUTPIXEL changes - changes to corelib made sdcard i/o unstable for unknown reasons and the performance improvement for PUTPIXEL was only about 10% --- lib/corelib.s | 189 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 156 insertions(+), 33 deletions(-) diff --git a/lib/corelib.s b/lib/corelib.s index 93dc81f..a21b95c 100644 --- a/lib/corelib.s +++ b/lib/corelib.s @@ -706,32 +706,108 @@ CMPWORDS_XT2: .EQU FB_PS $90C .EQU FB_PD $910 .EQU FB_CTL $914 - .EQU FB_SHIFTER $918 - .EQU FB_SHIFTCOUNT $91C - .EQU FB_SHIFTERM $920 - .EQU FB_SHIFTERSP $924 - .EQU FB_MASKGEN $928 +; set a pixel in fb memory +; parameters: x,y - coordinates +PUTPIXEL_1BPP: + ; calculate vmem address: + OVER ; duplicate x + ; divide x by 32 + SHR + SHR + SHR + SHR + SHR + SWAP + ; multiply y by words per line + SHL 2 + SHL 2 + SHL -; draw a single pixel -; args: x, y, color + ADD ; add results together for vmem addr + DUP + LOADCP FB_WA + SWAP + STOREI ; store to framebuffer write addr register + DROP + LOADCP FB_RA ; and to framebuffer read addr register + SWAP + STOREI + DROP + + ; x is now at top of stack + ; get bit value from x modulo 32 + LOADC 31 + AND + SHL 2 ; (x & 31) * 4 = offset into table + LOADCP INT_TO_PIX_TABLE + ADD + LOADI + + LOADCP FB_IO + ; read old vmem value + LOADCP FB_IO + LOADI + ; or in new bit + OR + ; write new value + STOREI + DROP + + RET + +INT_TO_PIX_TABLE: + .WORD %10000000_00000000_00000000_00000000 + .WORD %01000000_00000000_00000000_00000000 + .WORD %00100000_00000000_00000000_00000000 + .WORD %00010000_00000000_00000000_00000000 + .WORD %00001000_00000000_00000000_00000000 + .WORD %00000100_00000000_00000000_00000000 + .WORD %00000010_00000000_00000000_00000000 + .WORD %00000001_00000000_00000000_00000000 + .WORD %00000000_10000000_00000000_00000000 + .WORD %00000000_01000000_00000000_00000000 + .WORD %00000000_00100000_00000000_00000000 + .WORD %00000000_00010000_00000000_00000000 + .WORD %00000000_00001000_00000000_00000000 + .WORD %00000000_00000100_00000000_00000000 + .WORD %00000000_00000010_00000000_00000000 + .WORD %00000000_00000001_00000000_00000000 + .WORD %00000000_00000000_10000000_00000000 + .WORD %00000000_00000000_01000000_00000000 + .WORD %00000000_00000000_00100000_00000000 + .WORD %00000000_00000000_00010000_00000000 + .WORD %00000000_00000000_00001000_00000000 + .WORD %00000000_00000000_00000100_00000000 + .WORD %00000000_00000000_00000010_00000000 + .WORD %00000000_00000000_00000001_00000000 + .WORD %00000000_00000000_00000000_10000000 + .WORD %00000000_00000000_00000000_01000000 + .WORD %00000000_00000000_00000000_00100000 + .WORD %00000000_00000000_00000000_00010000 + .WORD %00000000_00000000_00000000_00001000 + .WORD %00000000_00000000_00000000_00000100 + .WORD %00000000_00000000_00000000_00000010 + .WORD %00000000_00000000_00000000_00000001 + +PUTMPIXEL: + LOADC 1 +; set a pixel in fb memory +; parameters: x,y,color - coordinates, color value (0-15) PUTPIXEL: PUTPIXEL_4BPP: .EQU PUTPIXEL_X 0 .EQU PUTPIXEL_Y 4 .EQU PUTPIXEL_COLOR 8 - .EQU PUTPIXEL_BPSAV 12 + .EQU PUTPIXEL_PIXPOS 12 .EQU PUTPIXEL_FS 16 FPADJ -PUTPIXEL_FS + STORE PUTPIXEL_COLOR STORE PUTPIXEL_Y STORE PUTPIXEL_X - LOADREG BP - STORE PUTPIXEL_BPSAV - LOADC 0 - STOREREG BP ; calculate vmem address: (x / 8) + (y * 80) LOAD PUTPIXEL_X @@ -750,36 +826,83 @@ PUTPIXEL_4BPP: ADD ; add results together for vmem addr - DUP - STORE.B FB_WA ; set as write and read addresses - STORE.B FB_RA + LOADCP FB_WA + OVER + STOREI ; store to framebuffer write addr register + DROP + LOADCP FB_RA ; and to framebuffer read addr register + SWAP ; swap addr and value for STOREI + STOREI + DROP + + LOAD PUTPIXEL_X + ; |0000.0000|0000.0000|0000.0000|0000.1111| + LOADC 7 + AND ; calculate pixel position in word + LOADC 7 + SWAP + SUB ; pixpos = 7 - (x & 7) + STORE PUTPIXEL_PIXPOS - ; create pixel data from color value in - ; leftmost pixel data bits (31-28) - LOADC 0 LOAD PUTPIXEL_COLOR - BPLC + LOAD PUTPIXEL_PIXPOS + SHR ; rcount = pixpos / 2 +ROTLOOP_: + DUP ; exit loop if rcount is 0 + CBRANCH.Z ROTLOOP_END + SWAP ; pixel value is now on top of stack + BROT ; value = value << 8 + SWAP ; rcount is now on top of stack + DEC 1 ; rcount = rcount - 1 + BRANCH ROTLOOP_ +ROTLOOP_END: + DROP ; drop rcount + ; shifted pixel value is now at top of stack + LOAD PUTPIXEL_PIXPOS + LOADC 1 + AND + CBRANCH.Z EVEN_PIXPOS + SHL 2 ; if pixpos is odd, shift by 4 bits SHL 2 - SHL 2 - STORE.B FB_SHIFTER ; store pixel into shifter +EVEN_PIXPOS: + LOAD PUTPIXEL_X + ; get bit value from x modulo 8 + LOADC 7 + AND + SHL 2 ; (x & 7) * 4 = offset into table + LOADCP INT_TO_MASK_TABLE + ADD + LOADI - LOAD PUTPIXEL_X ; use x coord as shift count - STORE.B FB_SHIFTCOUNT ; writing triggers shifting + ; read old vmem value + LOADCP FB_IO + LOADI + ; mask bits + AND + ; or in shifted pixel value + OR - LOAD.B FB_SHIFTERM ; get shift result as mask - LOAD.B FB_IO ; get background pixel data - AND ; remove bits for new pixel from bg - - LOAD.B FB_SHIFTER ; load shifted pixel - OR ; OR in new pixel bits - STORE.B FB_IO ; write new pixel data word to vmem - - LOAD PUTPIXEL_BPSAV - STOREREG BP + ; write new value + LOADCP FB_IO + SWAP + STOREI + DROP FPADJ PUTPIXEL_FS RET + .CPOOL + +INT_TO_MASK_TABLE: + .WORD %00001111_11111111_11111111_11111111 + .WORD %11110000_11111111_11111111_11111111 + .WORD %11111111_00001111_11111111_11111111 + .WORD %11111111_11110000_11111111_11111111 + .WORD %11111111_11111111_00001111_11111111 + .WORD %11111111_11111111_11110000_11111111 + .WORD %11111111_11111111_11111111_00001111 + .WORD %11111111_11111111_11111111_11110000 + ; draw a line between two points ; parameters: x0, y0, x1, y1, color .EQU DL_X0 0 From f90d52926f7a90f52a6a47e858b570ec99a063fe Mon Sep 17 00:00:00 2001 From: slederer Date: Sun, 1 Feb 2026 22:08:06 +0100 Subject: [PATCH 21/29] vgafb: simplify maskgen a bit to avoid timing problems --- examples/sprites.s | 3 +++ tridoracpu/tridoracpu.srcs/vgafb.v | 32 ++++++++++++++-------------- tridoracpu/tridoracpu.xpr | 34 ++++++++++++------------------ utils/tdrimg.py | 1 + 4 files changed, 33 insertions(+), 37 deletions(-) diff --git a/examples/sprites.s b/examples/sprites.s index ab2e580..5f50081 100644 --- a/examples/sprites.s +++ b/examples/sprites.s @@ -97,6 +97,7 @@ PS_LOOP1: STORE.B FB_SHIFTCOUNT LOAD.B FB_SHIFTERM ; get shifted mask + NOT LOAD.B FB_IO ; and background pixel data AND ; remove foreground pixels @@ -130,6 +131,7 @@ PS_NEXT_STRIPE: STORE.B FB_MASKGEN ; store to mask reg to get new mask LOAD.B FB_MASKGEN ; get mask for spill bits + shifted pixels + NOT LOAD.B FB_IO ; get vmem data AND ; remove foreground pixels from bg @@ -147,6 +149,7 @@ PS_NEXT_STRIPE: DUP STORE.B FB_MASKGEN LOAD.B FB_MASKGEN ; get sprite mask for spill bits + NOT LOAD.B FB_IO ; load next vmem word AND ; apply sprite mask diff --git a/tridoracpu/tridoracpu.srcs/vgafb.v b/tridoracpu/tridoracpu.srcs/vgafb.v index fd42627..49dad2d 100644 --- a/tridoracpu/tridoracpu.srcs/vgafb.v +++ b/tridoracpu/tridoracpu.srcs/vgafb.v @@ -341,28 +341,28 @@ module vgafb #(VMEM_ADDR_WIDTH = 15, VMEM_DATA_WIDTH = 32) ( always @(posedge cpu_clk) begin acc_mask_buf <= { - {4{~|{acc_mask_in[31:28]}}}, - {4{~|{acc_mask_in[27:24]}}}, - {4{~|{acc_mask_in[23:20]}}}, - {4{~|{acc_mask_in[19:16]}}}, - {4{~|{acc_mask_in[15:12]}}}, - {4{~|{acc_mask_in[11:8]}}}, - {4{~|{acc_mask_in[7:4]}}}, - {4{~|{acc_mask_in[3:0]}}} + {4{|{acc_mask_in[31:28]}}}, + {4{|{acc_mask_in[27:24]}}}, + {4{|{acc_mask_in[23:20]}}}, + {4{|{acc_mask_in[19:16]}}}, + {4{|{acc_mask_in[15:12]}}}, + {4{|{acc_mask_in[11:8]}}}, + {4{|{acc_mask_in[7:4]}}}, + {4{|{acc_mask_in[3:0]}}} }; end always @(posedge cpu_clk) begin acc_shiftmask_buf = { - {4{~|{acc_shifter_out_h[31:28]}}}, - {4{~|{acc_shifter_out_h[27:24]}}}, - {4{~|{acc_shifter_out_h[23:20]}}}, - {4{~|{acc_shifter_out_h[19:16]}}}, - {4{~|{acc_shifter_out_h[15:12]}}}, - {4{~|{acc_shifter_out_h[11:8]}}}, - {4{~|{acc_shifter_out_h[7:4]}}}, - {4{~|{acc_shifter_out_h[3:0]}}} + {4{|{acc_shifter_out_h[31:28]}}}, + {4{|{acc_shifter_out_h[27:24]}}}, + {4{|{acc_shifter_out_h[23:20]}}}, + {4{|{acc_shifter_out_h[19:16]}}}, + {4{|{acc_shifter_out_h[15:12]}}}, + {4{|{acc_shifter_out_h[11:8]}}}, + {4{|{acc_shifter_out_h[7:4]}}}, + {4{|{acc_shifter_out_h[3:0]}}} }; end `endif diff --git a/tridoracpu/tridoracpu.xpr b/tridoracpu/tridoracpu.xpr index a088319..5d8ff88 100644 --- a/tridoracpu/tridoracpu.xpr +++ b/tridoracpu/tridoracpu.xpr @@ -356,14 +356,12 @@ - + - - - - - - + + Vivado Synthesis Defaults + + @@ -380,24 +378,18 @@ - + - + + Default settings for Implementation. + - - - + - - - + - - - - - - + + diff --git a/utils/tdrimg.py b/utils/tdrimg.py index b7ce4cb..4eeaead 100644 --- a/utils/tdrimg.py +++ b/utils/tdrimg.py @@ -614,6 +614,7 @@ def create_image_with_stuff(imgfile): slotnr = putfile("../examples/benchmarks.pas", None , f, part, partstart, slotnr) slotnr = putfile("../examples/animate.pas", None , f, part, partstart, slotnr) + slotnr = putfile("../examples/graphbench.pas", None , f, part, partstart, slotnr) slotnr = putfile("../examples/sprites.inc", None , f, part, partstart, slotnr) slotnr = putfile("../examples/sprites.s", None , f, part, partstart, slotnr) slotnr = putfile("../examples/background.pict", None , f, part, partstart, slotnr) From 885e50c1c09838ca19f8560774cf225011f169f4 Mon Sep 17 00:00:00 2001 From: slederer Date: Sun, 1 Feb 2026 22:46:18 +0100 Subject: [PATCH 22/29] corelib: restore new PUTPIXEL implementation --- lib/corelib.s | 190 +++++++++----------------------------------------- 1 file changed, 34 insertions(+), 156 deletions(-) diff --git a/lib/corelib.s b/lib/corelib.s index a21b95c..c57a94e 100644 --- a/lib/corelib.s +++ b/lib/corelib.s @@ -706,108 +706,32 @@ CMPWORDS_XT2: .EQU FB_PS $90C .EQU FB_PD $910 .EQU FB_CTL $914 -; set a pixel in fb memory -; parameters: x,y - coordinates -PUTPIXEL_1BPP: - ; calculate vmem address: - OVER ; duplicate x - ; divide x by 32 - SHR - SHR - SHR - SHR - SHR - SWAP - ; multiply y by words per line - SHL 2 - SHL 2 - SHL + .EQU FB_SHIFTER $918 + .EQU FB_SHIFTCOUNT $91C + .EQU FB_SHIFTERM $920 + .EQU FB_SHIFTERSP $924 + .EQU FB_MASKGEN $928 - ADD ; add results together for vmem addr +; draw a single pixel +; args: x, y, color - DUP - LOADCP FB_WA - SWAP - STOREI ; store to framebuffer write addr register - DROP - LOADCP FB_RA ; and to framebuffer read addr register - SWAP - STOREI - DROP - - ; x is now at top of stack - ; get bit value from x modulo 32 - LOADC 31 - AND - SHL 2 ; (x & 31) * 4 = offset into table - LOADCP INT_TO_PIX_TABLE - ADD - LOADI - - LOADCP FB_IO - ; read old vmem value - LOADCP FB_IO - LOADI - ; or in new bit - OR - ; write new value - STOREI - DROP - - RET - -INT_TO_PIX_TABLE: - .WORD %10000000_00000000_00000000_00000000 - .WORD %01000000_00000000_00000000_00000000 - .WORD %00100000_00000000_00000000_00000000 - .WORD %00010000_00000000_00000000_00000000 - .WORD %00001000_00000000_00000000_00000000 - .WORD %00000100_00000000_00000000_00000000 - .WORD %00000010_00000000_00000000_00000000 - .WORD %00000001_00000000_00000000_00000000 - .WORD %00000000_10000000_00000000_00000000 - .WORD %00000000_01000000_00000000_00000000 - .WORD %00000000_00100000_00000000_00000000 - .WORD %00000000_00010000_00000000_00000000 - .WORD %00000000_00001000_00000000_00000000 - .WORD %00000000_00000100_00000000_00000000 - .WORD %00000000_00000010_00000000_00000000 - .WORD %00000000_00000001_00000000_00000000 - .WORD %00000000_00000000_10000000_00000000 - .WORD %00000000_00000000_01000000_00000000 - .WORD %00000000_00000000_00100000_00000000 - .WORD %00000000_00000000_00010000_00000000 - .WORD %00000000_00000000_00001000_00000000 - .WORD %00000000_00000000_00000100_00000000 - .WORD %00000000_00000000_00000010_00000000 - .WORD %00000000_00000000_00000001_00000000 - .WORD %00000000_00000000_00000000_10000000 - .WORD %00000000_00000000_00000000_01000000 - .WORD %00000000_00000000_00000000_00100000 - .WORD %00000000_00000000_00000000_00010000 - .WORD %00000000_00000000_00000000_00001000 - .WORD %00000000_00000000_00000000_00000100 - .WORD %00000000_00000000_00000000_00000010 - .WORD %00000000_00000000_00000000_00000001 - -PUTMPIXEL: - LOADC 1 -; set a pixel in fb memory -; parameters: x,y,color - coordinates, color value (0-15) PUTPIXEL: PUTPIXEL_4BPP: .EQU PUTPIXEL_X 0 .EQU PUTPIXEL_Y 4 .EQU PUTPIXEL_COLOR 8 - .EQU PUTPIXEL_PIXPOS 12 + .EQU PUTPIXEL_BPSAV 12 .EQU PUTPIXEL_FS 16 FPADJ -PUTPIXEL_FS - STORE PUTPIXEL_COLOR STORE PUTPIXEL_Y STORE PUTPIXEL_X + LOADREG BP + STORE PUTPIXEL_BPSAV + LOADC 0 + STOREREG BP ; calculate vmem address: (x / 8) + (y * 80) LOAD PUTPIXEL_X @@ -826,83 +750,37 @@ PUTPIXEL_4BPP: ADD ; add results together for vmem addr - LOADCP FB_WA - OVER - STOREI ; store to framebuffer write addr register - DROP - LOADCP FB_RA ; and to framebuffer read addr register - SWAP ; swap addr and value for STOREI - STOREI - DROP - - LOAD PUTPIXEL_X - ; |0000.0000|0000.0000|0000.0000|0000.1111| - LOADC 7 - AND ; calculate pixel position in word - LOADC 7 - SWAP - SUB ; pixpos = 7 - (x & 7) - STORE PUTPIXEL_PIXPOS + DUP + STORE.B FB_WA ; set as write and read addresses + STORE.B FB_RA + ; create pixel data from color value in + ; leftmost pixel data bits (31-28) + LOADC 0 LOAD PUTPIXEL_COLOR - LOAD PUTPIXEL_PIXPOS - SHR ; rcount = pixpos / 2 -ROTLOOP_: - DUP ; exit loop if rcount is 0 - CBRANCH.Z ROTLOOP_END - SWAP ; pixel value is now on top of stack - BROT ; value = value << 8 - SWAP ; rcount is now on top of stack - DEC 1 ; rcount = rcount - 1 - BRANCH ROTLOOP_ -ROTLOOP_END: - DROP ; drop rcount - ; shifted pixel value is now at top of stack - LOAD PUTPIXEL_PIXPOS - LOADC 1 - AND - CBRANCH.Z EVEN_PIXPOS - SHL 2 ; if pixpos is odd, shift by 4 bits + BPLC SHL 2 -EVEN_PIXPOS: - LOAD PUTPIXEL_X - ; get bit value from x modulo 8 - LOADC 7 - AND - SHL 2 ; (x & 7) * 4 = offset into table - LOADCP INT_TO_MASK_TABLE - ADD - LOADI + SHL 2 + STORE.B FB_SHIFTER ; store pixel into shifter - ; read old vmem value - LOADCP FB_IO - LOADI - ; mask bits - AND - ; or in shifted pixel value - OR + LOAD PUTPIXEL_X ; use x coord as shift count + STORE.B FB_SHIFTCOUNT ; writing triggers shifting - ; write new value - LOADCP FB_IO - SWAP - STOREI - DROP + LOAD.B FB_SHIFTERM ; get shift result as mask + NOT ; invert to get background mask + LOAD.B FB_IO ; get background pixel data + AND ; remove bits for new pixel from bg + + LOAD.B FB_SHIFTER ; load shifted pixel + OR ; OR in new pixel bits + STORE.B FB_IO ; write new pixel data word to vmem + + LOAD PUTPIXEL_BPSAV + STOREREG BP FPADJ PUTPIXEL_FS RET - .CPOOL - -INT_TO_MASK_TABLE: - .WORD %00001111_11111111_11111111_11111111 - .WORD %11110000_11111111_11111111_11111111 - .WORD %11111111_00001111_11111111_11111111 - .WORD %11111111_11110000_11111111_11111111 - .WORD %11111111_11111111_00001111_11111111 - .WORD %11111111_11111111_11110000_11111111 - .WORD %11111111_11111111_11111111_00001111 - .WORD %11111111_11111111_11111111_11110000 - ; draw a line between two points ; parameters: x0, y0, x1, y1, color .EQU DL_X0 0 From 4ad879ba68b4153d83df94f08c9d372c34679a27 Mon Sep 17 00:00:00 2001 From: slederer Date: Sun, 1 Feb 2026 23:27:25 +0100 Subject: [PATCH 23/29] Update documentation --- LICENSE.md | 2 +- doc/mem.md | 5 ++-- doc/tdraudio.md | 10 ++++---- doc/vga.md | 68 ++++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 70 insertions(+), 15 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 3755dbb..6392510 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -4,7 +4,7 @@ All files, except where explicitly stated otherwise, are licensed according to t ------------------------------------------------------------------------------ -Copyright 2024 Sebastian Lederer +Copyright 2024-2026 Sebastian Lederer Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/doc/mem.md b/doc/mem.md index f7dbc2b..29177b2 100644 --- a/doc/mem.md +++ b/doc/mem.md @@ -22,11 +22,12 @@ The _BSEL_ and _BPLC_ instructions are designed to assist with accessing bytes w The byte ordering is big-endian. ## Accessing the I/O Area -The I/O area organizes memory slightly different. Here, pointing out individual bytes is not very useful, so the I/O controllers use register addresses with increments of one. In practice, there is only the VGA framebuffer controller which uses multiple registers. +The I/O area uses the same word addressing in increments of four to access the registers of the I/O controllers. In practice, only the VGA framebuffer controller and the audio controller use multiple registers. +For the other controllers, there is a single 32 bit register that is repeated all over the address space of the corresponding I/O slot. The individual I/O controllers each have a memory area of 128 bytes, so there is a maximum number of 16 I/O controllers. -Currently, only I/O slots 0-3 are being used. +Currently, only I/O slots 0-4 are being used. |I/O slot| Address | Controller | |--------|---------|------------| diff --git a/doc/tdraudio.md b/doc/tdraudio.md index 999ebfc..5d8b22f 100644 --- a/doc/tdraudio.md +++ b/doc/tdraudio.md @@ -10,12 +10,12 @@ For the first channel the register addresses are: |Address|Description| |-------|-----------| | $A00 | Control Register | -| $A01 | Clock Divider Register | -| $A02 | Amplitude Register | +| $A04 | Clock Divider Register | +| $A08 | Amplitude Register | -The register addresses for the second channel start at $A04, -the third channel at $A08 -and the fourth channel at $A0C. +The register addresses for the second channel start at $A10, +the third channel at $A20 +and the fourth channel at $A30. ## Reading the control register diff --git a/doc/vga.md b/doc/vga.md index b53f56d..76520f2 100644 --- a/doc/vga.md +++ b/doc/vga.md @@ -4,13 +4,16 @@ Registers |Name|Address|Description| |----|-------|-----------| |_FB_RA_ | $900 | Read Address | -|_FB_WA_ | $901 | Write Address | -| _FB_IO_ | $902 | I/O Register | -| _FB_PS_ | $903 | Palette Select | -| _FB_PD_ | $904 | Palette Data | -| _FB_CTL_ | $905 | Control Register | - - +|_FB_WA_ | $904 | Write Address | +| _FB_IO_ | $908 | I/O Register | +| _FB_PS_ | $90C | Palette Select | +| _FB_PD_ | $910 | Palette Data | +| _FB_CTL_ | $914 | Control Register | +| _FB_SHIFTER | $918 | Shift Assist Register | +| _FB_SHIFTCOUNT | $91C | Shift Count Register | +| _FB_SHIFTERM | $920 | Shifted Mask Register | +| _FB_SHIFTERSP | $924 | Shifter Spill Register | +| _FB_MASKGEN | $928 | Mask Generator Register | ## Pixel Data Pixel data is organized in 32-bit-words. With four bits per pixel, one word @@ -81,3 +84,54 @@ The control register contains status information. It can only be read. The _m_ field indicates the current graphics mode. At the time of writing, it is always 1 which denotes a 640x400x4 mode. The _vb_ bit is 1 when the video signal generator is in its vertical blank phase. + +## Shift Assist Register +The *shift assist register* can be used to accelerate shifting pixel/bitmap data. +Writing a word of pixel data to this register initialises the shifting process. + +After writing to the shift count register (see below), reading the shift assist +register retrieves the shifted pixel data. + +Writing to the shift assist register will reset the shift count. + +## Shift Count Register +Writing a number from 0-7 to the *shift count register* triggers shifting the +contents of the shift assist register. Pixel data is shifted by four bits +to the right times the shift count. Bits 31-3 of the shift count are ignored, so you can +directly write a horizontal screen coordinate to the register. + +This register cannot be read. + +## Shifter Mask Register +The *shifter mask register* contains the shifted pixel data converted into +a mask. See the *mask generator register* for an +explanation of the mask. + +## Shifter Spill Register +The *shifter spill register* contains the pixel data that has +been shifted out to the right. For example, if the shift count is two, +the spill register contains the two rightmost pixels (bits 7-0) of +the original pixel data, placed into the two topmost pixels (bits 31-24). + +The rest of the register is set to zero. + +## Mask Generator Register +The *mask generator register* creates a mask from pixel data. +For each four bits of a pixel, the corresponding four mask bits +are all set to one if the pixel value is not zero. + +This can be used to combine foreground and background pixel data +with a pixel value of zero for a transparent background color. + +Usually, the mask will be inverted with a *NOT* instruction +to clear all pixels in the background that are set in the foreground +with an *AND* instruction +before *ORing* foreground and background together. + +Example in hexadecimal, each digit is a pixel: +| Pixel Data | Mask | +|------------|------| +| $00000000 | $00000000 | +| $00000001 | $0000000F | +| $0407000F | $0F0F000F | +| $1234ABC0 | $FFFFFFF0 | From 4d103f99ec041a5e50ec5fbf64b8dcdac144d7ec Mon Sep 17 00:00:00 2001 From: slederer Date: Mon, 2 Feb 2026 00:33:50 +0100 Subject: [PATCH 24/29] corelib: PUTPIXEL can draw color 0 again --- lib/corelib.s | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/corelib.s b/lib/corelib.s index c57a94e..1ac12e9 100644 --- a/lib/corelib.s +++ b/lib/corelib.s @@ -754,6 +754,9 @@ PUTPIXEL_4BPP: STORE.B FB_WA ; set as write and read addresses STORE.B FB_RA + LOAD PUTPIXEL_COLOR + CBRANCH.Z PUTPX_CLR ; color 0 is special case + ; create pixel data from color value in ; leftmost pixel data bits (31-28) LOADC 0 @@ -775,12 +778,29 @@ PUTPIXEL_4BPP: OR ; OR in new pixel bits STORE.B FB_IO ; write new pixel data word to vmem +PUTPX_XT: LOAD PUTPIXEL_BPSAV STOREREG BP FPADJ PUTPIXEL_FS RET +PUTPX_CLR: + LOADCP $F0000000 ; mask for leftmost pixel + STORE.B FB_SHIFTER ; shift accordingly + LOAD PUTPIXEL_X + STORE.B FB_SHIFTCOUNT + + LOAD.B FB_SHIFTER ; get shifted value + NOT ; invert for real mask + LOAD.B FB_IO ; get background pixels + AND ; clear pixel with mask + STORE.B FB_IO ; no need to OR in new pixel, just store to vmem + + BRANCH PUTPX_XT + + + ; draw a line between two points ; parameters: x0, y0, x1, y1, color .EQU DL_X0 0 From e8e4b5dd2475943c7a4500b1af3d67f7b4eae414 Mon Sep 17 00:00:00 2001 From: slederer Date: Sun, 8 Feb 2026 01:58:50 +0100 Subject: [PATCH 25/29] tridoraemu,docs: emulate vgafb shifter/maskgen and new iomem layout --- doc/vga.md | 20 +++++------ tridoraemu/cpu.go | 1 + tridoraemu/framebuffer.go | 72 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 78 insertions(+), 15 deletions(-) diff --git a/doc/vga.md b/doc/vga.md index 76520f2..3ef8f8d 100644 --- a/doc/vga.md +++ b/doc/vga.md @@ -9,11 +9,11 @@ Registers | _FB_PS_ | $90C | Palette Select | | _FB_PD_ | $910 | Palette Data | | _FB_CTL_ | $914 | Control Register | -| _FB_SHIFTER | $918 | Shift Assist Register | -| _FB_SHIFTCOUNT | $91C | Shift Count Register | -| _FB_SHIFTERM | $920 | Shifted Mask Register | -| _FB_SHIFTERSP | $924 | Shifter Spill Register | -| _FB_MASKGEN | $928 | Mask Generator Register | +| _FB_SHIFTER_ | $918 | Shift Assist Register | +| _FB_SHIFTCOUNT_ | $91C | Shift Count Register | +| _FB_SHIFTERM_ | $920 | Shifted Mask Register | +| _FB_SHIFTERSP_ | $924 | Shifter Spill Register | +| _FB_MASKGEN_ | $928 | Mask Generator Register | ## Pixel Data Pixel data is organized in 32-bit-words. With four bits per pixel, one word @@ -121,12 +121,12 @@ For each four bits of a pixel, the corresponding four mask bits are all set to one if the pixel value is not zero. This can be used to combine foreground and background pixel data -with a pixel value of zero for a transparent background color. +where a pixel value of zero is used to indicate a transparent foreground pixel. -Usually, the mask will be inverted with a *NOT* instruction -to clear all pixels in the background that are set in the foreground -with an *AND* instruction -before *ORing* foreground and background together. +Usually, the mask will be inverted with a *NOT* instruction. +The result can then be used to clear all pixels in the background +that are set in the foreground, using an *AND* instruction. +As the last step, foreground and masked background data can be combined with an *OR* instruction. Example in hexadecimal, each digit is a pixel: | Pixel Data | Mask | diff --git a/tridoraemu/cpu.go b/tridoraemu/cpu.go index 9d6b08d..f1ae4de 100644 --- a/tridoraemu/cpu.go +++ b/tridoraemu/cpu.go @@ -249,6 +249,7 @@ func (c *CPU) step() error { var name string if (insWord & 1) == 1 { name = "STORE.B" + operand &= ^1 ea = c.BP + word(operand) } else { name = "STORE" diff --git a/tridoraemu/framebuffer.go b/tridoraemu/framebuffer.go index 189100a..71776ca 100644 --- a/tridoraemu/framebuffer.go +++ b/tridoraemu/framebuffer.go @@ -10,11 +10,16 @@ import ( const VmemWords = 32768 const PaletteSlots = 16 const FB_RA = 0 -const FB_WA = 1 -const FB_IO = 2 -const FB_PS = 3 -const FB_PD = 4 -const FB_CTL= 5 +const FB_WA = 4 +const FB_IO = 8 +const FB_PS = 12 +const FB_PD = 16 +const FB_CTL= 20 +const FB_SHIFTER = 24 +const FB_SHIFTCOUNT = 28 +const FB_SHIFTERM = 32 +const FB_SHIFTERSP = 36 +const FB_MASKGEN = 40 const PixelMask = 0b11110000000000000000000000000000 const PixelPerWord = 8 @@ -33,6 +38,9 @@ type Framebuffer struct { vmem [VmemWords]word readCount int paletteChanged bool + shiftAssistData word + shiftAssistCount int + maskGenData word } func (f *Framebuffer) initialize() { @@ -53,6 +61,11 @@ func (f *Framebuffer) read(byteaddr word) (word, error) { case FB_PS: result = f.paletteSlot case FB_PD: result = f.readPalette() case FB_CTL: result = f.readCtl() + case FB_SHIFTER: result = f.readShiftAssist() + case FB_SHIFTCOUNT: result = 0xFFFFFFF + case FB_SHIFTERM: result = f.readShifterM() + case FB_SHIFTERSP: result = f.readShifterSp() + case FB_MASKGEN: result = f.readMaskGen() default: } return result, nil @@ -67,6 +80,11 @@ func (f *Framebuffer) write(value word, byteaddr word) (error) { case FB_PS: f.paletteSlot = value case FB_PD: f.writePalette(value) case FB_CTL: f.writeCtl(value) + case FB_SHIFTER: f.writeShiftAssist(value) + case FB_SHIFTCOUNT: f.writeShiftCount(value) + case FB_SHIFTERM: + case FB_SHIFTERSP: + case FB_MASKGEN: f.writeMaskGen(value) default: } @@ -152,3 +170,47 @@ func (f *Framebuffer) readCtl() word { func (f *Framebuffer) writeCtl(value word) { } + +func (f *Framebuffer) writeShiftAssist(value word) { + f.shiftAssistData = value + f.shiftAssistCount = 0 +} + +func (f *Framebuffer) readShiftAssist() word { + return f.shiftAssistData >> (f.shiftAssistCount * 4) +} + +func (f *Framebuffer) writeShiftCount(value word) { + f.shiftAssistCount = int(value & 0x7) +} + +func (f *Framebuffer) readShifterM() word { + return convertToMask(f.readShiftAssist()) +} + +func pixelToMask(pixels word, mask word) word { + if (pixels & mask) != 0 { return mask } else { return 0 } +} + +func convertToMask(pixels word) word { + return pixelToMask(pixels, 0xF0000000) | + pixelToMask(pixels, 0x0F000000) | + pixelToMask(pixels, 0x00F00000) | + pixelToMask(pixels, 0x000F0000) | + pixelToMask(pixels, 0x0000F000) | + pixelToMask(pixels, 0x00000F00) | + pixelToMask(pixels, 0x000000F0) | + pixelToMask(pixels, 0x0000000F) +} + +func (f *Framebuffer) readShifterSp() word { + return word(f.shiftAssistData << ((8-f.shiftAssistCount)*4)) +} + +func (f *Framebuffer) writeMaskGen(value word) { + f.maskGenData = value +} + +func (f *Framebuffer) readMaskGen() word { + return convertToMask(f.maskGenData) +} From 6c27b78c4a219839edbedf8fb7b550a7bec4e06d Mon Sep 17 00:00:00 2001 From: slederer Date: Mon, 16 Feb 2026 02:40:20 +0100 Subject: [PATCH 26/29] corelib: improved default palette - light gray instead of 2x dark gray - add palette file for paint programs - new sprite with default palette for graphbench --- examples/default-palette.pal | 19 +++++++++++++++++++ examples/graphbench.pas | 2 +- examples/sprite-testcard.sprt | Bin 0 -> 520 bytes lib/corelib.s | 2 +- 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 examples/default-palette.pal create mode 100644 examples/sprite-testcard.sprt diff --git a/examples/default-palette.pal b/examples/default-palette.pal new file mode 100644 index 0000000..d41cbbe --- /dev/null +++ b/examples/default-palette.pal @@ -0,0 +1,19 @@ +JASC-PAL +0100 +16 +0 0 0 +255 255 255 +255 0 0 +0 255 0 +0 0 255 +0 255 255 +255 0 255 +255 255 0 +127 127 127 +160 160 160 +127 0 0 +0 127 0 +0 0 127 +0 127 127 +127 0 127 +127 127 0 diff --git a/examples/graphbench.pas b/examples/graphbench.pas index 9abbfba..32f4627 100644 --- a/examples/graphbench.pas +++ b/examples/graphbench.pas @@ -106,7 +106,7 @@ begin end; begin - readSpriteData('rocket.sprt'); + readSpriteData('sprite-testcard.sprt'); InitGraphics; startBench('points 200K'); diff --git a/examples/sprite-testcard.sprt b/examples/sprite-testcard.sprt new file mode 100644 index 0000000000000000000000000000000000000000..e31fc9e4ffbfb41c65b9b1abcffb3822382740b2 GIT binary patch literal 520 zcmZvZyAi@L42G4`Wjb8K3M|3_;VuiX00p%>sk57$wE`VA3sA5E3sA5D2T~rydFalT zKmC&JlXjc!9sn{~GiI255@w%);eS9*TGs`Poa=kq5_L7J08sDEJE|~$KhN9s{klBv zghR#%5EFgEG}5PRD6L07VMHRz^W0i1yElj!V+?97i%w4DSskKVIOl>a7ud>w5)LC) zol-pjl!xQ_v|g^5^<{vf>RJ0yls!k~L)GOI3O5Z+`>lge7T4NnUQ#lgXO}1b2U>Kj AYXATM literal 0 HcmV?d00001 diff --git a/lib/corelib.s b/lib/corelib.s index 1ac12e9..998cc8c 100644 --- a/lib/corelib.s +++ b/lib/corelib.s @@ -977,7 +977,7 @@ SETPALETTE: DEFAULT_PALETTE: .WORD 0, $FFF, $F00, $0F0, $00F, $0FF, $F0F, $FF0 - .WORD $777, $777, $700, $070, $007, $077, $707, $770 + .WORD $777, $AAA, $700, $070, $007, $077, $707, $770 ; set whole video memory to zero CLEARGRAPHICS: From fdc5d05d64328b8ab750899b0a3d2a3bdfe75e11 Mon Sep 17 00:00:00 2001 From: slederer Date: Mon, 23 Feb 2026 00:37:32 +0100 Subject: [PATCH 27/29] examples/animate: update animation demo --- examples/animate.pas | 68 ++++++++++++++++++++++++++++++++++----- examples/background.pict | Bin 128072 -> 128072 bytes examples/rocket.sprt | Bin 2056 -> 2056 bytes examples/walking.sprt | Bin 2056 -> 2056 bytes 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/examples/animate.pas b/examples/animate.pas index 0e4bb73..92888a2 100644 --- a/examples/animate.pas +++ b/examples/animate.pas @@ -15,6 +15,8 @@ type PictData = record frameCount:integer; frameTime:integer; frameLeft:integer; + moveTime:integer; + moveLeft:integer; changed:boolean; frame:array [0..3] of SpritePixels; end; @@ -25,6 +27,8 @@ var pic:PictData; ch:char; stickMan:Sprite; rocket:Sprite; + rocket2:Sprite; + rocket3:Sprite; procedure WaitVSync; external; @@ -55,6 +59,7 @@ end; procedure animateSprite(var aSprite:Sprite); var frameIndex:integer; frameTime,frameLeft:integer; + moveTime,moveLeft:integer; ydelta:integer; oldX,oldY:integer; begin @@ -62,6 +67,8 @@ begin frameIndex := aSprite.curFrame; frameTime := aSprite.frameTime; frameLeft := aSprite.frameLeft; + moveTime := aSprite.moveTime; + moveLeft := aSprite.moveLeft; oldX := aSprite.x; oldY := aSprite.y; aSprite.oldX := oldX; aSprite.oldY := oldY; @@ -69,16 +76,19 @@ begin if frameLeft <= 0 then begin frameIndex := frameIndex + 1; - frameLeft := aSPrite.frameTime; + frameLeft := aSprite.frameTime; aSprite.frameLeft := frameLeft; aSprite.curFrame := frameIndex; if frameIndex >= aSprite.frameCount then aSprite.curFrame := 0; + end; - aSprite.frameLeft := frameLeft; - + moveLeft := moveLeft -1; + if moveLeft <= 0 then + begin aSprite.x := aSprite.x + aSprite.xdelta; aSprite.y := aSprite.y + aSprite.ydelta; + moveLeft := moveTime; if aSprite.x > 608 then aSprite.x := 0; @@ -88,30 +98,57 @@ begin aSprite.x := 0; end; end; + aSprite.frameLeft := frameLeft; + aSprite.moveLeft := moveLeft; end; procedure animLoop; var i:integer; oldX,oldY:integer; roldX,roldY:integer; + r2oldX,r2oldY:integer; + r3oldX,r3oldY:integer; begin stickMan.x := 0; - stickMan.y := 310; + stickMan.y := 322; stickMan.frameTime := 6; stickMan.frameLeft := stickMan.frameTime; stickMan.curFrame := 0; stickMan.xdelta := 2; stickMan.ydelta := 0; - + stickMan.moveTime := 2; + stickman.moveLeft := stickMan.moveTime; rocket.x := 0; rocket.y := 200; - rocket.frameTime := 1; + rocket.frameTime := 5; rocket.frameLeft := rocket.frameTime; rocket.curFrame := 0; - rocket.xdelta := 2; + rocket.xdelta := 3; rocket.ydelta := -1; + rocket.moveTime := 1; + rocket.moveLeft := rocket.moveTime; + + rocket2.x := 50; + rocket2.y := 100; + rocket2.frameTime := 5; + rocket2.frameLeft := rocket2.frameTime; + rocket2.curFrame := 0; + rocket2.xdelta := 3; + rocket2.ydelta := -1; + rocket2.moveTime := 1; + rocket2.moveLeft := rocket2.moveTime; + + rocket3.x :=100; + rocket3.y := 50; + rocket3.frameTime := 5; + rocket3.frameLeft := rocket3.frameTime; + rocket3.curFrame := 0; + rocket3.xdelta := 3; + rocket3.ydelta := -1; + rocket3.moveTime := 1; + rocket3.moveLeft := rocket3.moveTime; while not ConAvail do begin @@ -121,17 +158,29 @@ begin roldX := rocket.x; roldY := rocket.y; + r2oldX := rocket2.x; + r2oldY := rocket2.y; + + r3oldX := rocket3.x; + r3oldY := rocket3.y; + PutSprite(roldX, roldY, rocket.frame[rocket.curFrame]); + PutSprite(r2oldX, r2oldY, rocket2.frame[rocket2.curFrame]); + PutSprite(r3oldX, r3oldY, rocket3.frame[rocket3.curFrame]); PutSprite(oldX, oldY, stickMan.frame[stickMan.curFrame]); animateSprite(rocket); + animateSprite(rocket2); + animateSprite(rocket3); animateSprite(stickMan); - {Delay(1);} + Delay(10); WaitVSync; UndrawSprite(oldX, oldY, pic.pixeldata); UndrawSprite(roldX, roldY, pic.pixeldata); + UndrawSprite(r2oldX, r2oldY, pic.pixeldata); + UndrawSprite(r3oldX, r3oldY, pic.pixeldata); end; end; @@ -160,5 +209,8 @@ begin loadSpriteFrame(rocket, 3, infile, 3); close(infile); + rocket2.frame := rocket.frame; + rocket3.frame := rocket.frame; + animLoop; end. diff --git a/examples/background.pict b/examples/background.pict index 55e7a2c02072cc8b4a8a1b921466d67bea03eeb2..b9453473e120734f8bfe7f03dbe5992bc7b4bc52 100644 GIT binary patch literal 128072 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 literal 128072 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%0D3v9;bm7JTc661-$8R(DEa-l5hjCJ#kkMAZWL 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 literal 2056 zcmeH{u};G<5J1mDcamOxI`~e795KER?^4x)6U|>cHU9uAL1GEfCjPOyM z<22Y^SU^;$mXhe{Y@efd=ZmxRO8{W>{L)~EcrxzIFp)GyM&U5uGLs@vcgvm;0cdin zZkK}Ms1%}D;q@ zd0#(L%8~2Z*nqh)l{->79M!LaG>@1K$%p7(e*UX}s3QMB`U3yJZQBI?fePR?@DEe~ zZQvU$6+p-{`!$bf7 n-TnWaf3xG?#{Ny+hW*d&s;+~2p!V+tkM}>PYKZ(h{5k)hxWAXA diff --git a/examples/walking.sprt b/examples/walking.sprt index 2cdd0ceda91d8f52c7e9e6e4471d8620271d1dea..a4411d44658107f48c8d04a1fafd18a92989a9fb 100644 GIT binary patch literal 2056 zcmd5-2@1n74D(*#uZ?a0e?xwd$tlE^S_mtJHKh>6jt?nm-_O^_%pQ0z8NE>$Z@l1U zm8+)A$ADCbo9~ literal 2056 zcmd5-2@1n74D;}Z0{O_;_W!p`P9e6`LRcxRDTOF@d`Lv-yrjIp~i z6xH?{Jhnp6JWG)G7v@`RY=`Hl__?L3) Date: Sun, 1 Mar 2026 00:25:37 +0100 Subject: [PATCH 28/29] examples: tweak animation demo, add assets for graphbench --- examples/animate.pas | 13 ++++--------- utils/tdrimg.py | 3 +++ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/examples/animate.pas b/examples/animate.pas index 92888a2..f262c46 100644 --- a/examples/animate.pas +++ b/examples/animate.pas @@ -9,7 +9,6 @@ type PictData = record Sprite = record x,y:integer; - oldX,oldY:integer; xdelta,ydelta:integer; curFrame:integer; frameCount:integer; @@ -61,7 +60,6 @@ var frameIndex:integer; frameTime,frameLeft:integer; moveTime,moveLeft:integer; ydelta:integer; - oldX,oldY:integer; begin ydelta := aSprite.ydelta; frameIndex := aSprite.curFrame; @@ -69,21 +67,18 @@ begin frameLeft := aSprite.frameLeft; moveTime := aSprite.moveTime; moveLeft := aSprite.moveLeft; - 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; + frameLeft := frameTime; aSprite.curFrame := frameIndex; if frameIndex >= aSprite.frameCount then aSprite.curFrame := 0; end; - moveLeft := moveLeft -1; + moveLeft := moveLeft - 1; if moveLeft <= 0 then begin aSprite.x := aSprite.x + aSprite.xdelta; @@ -209,8 +204,8 @@ begin loadSpriteFrame(rocket, 3, infile, 3); close(infile); - rocket2.frame := rocket.frame; - rocket3.frame := rocket.frame; + rocket2 := rocket; rocket2.curFrame := 1; + rocket3 := rocket; rocket3.curFrame := 2; animLoop; end. diff --git a/utils/tdrimg.py b/utils/tdrimg.py index 4eeaead..c03e59b 100644 --- a/utils/tdrimg.py +++ b/utils/tdrimg.py @@ -620,6 +620,9 @@ def create_image_with_stuff(imgfile): slotnr = putfile("../examples/background.pict", None , f, part, partstart, slotnr) slotnr = putfile("../examples/walking.sprt", None , f, part, partstart, slotnr) slotnr = putfile("../examples/rocket.sprt", None , f, part, partstart, slotnr) + slotnr = putfile("../examples/sprite-testcard.sprt", None , f, part, partstart, slotnr) + slotnr = putfile("../examples/tiles.inc", None , f, part, partstart, slotnr) + slotnr = putfile("../examples/tiles.s", None , f, part, partstart, slotnr) listdir(f, part) From 819c808c50f18dbf4f35dc38e6ab560127d1a396 Mon Sep 17 00:00:00 2001 From: slederer Date: Sun, 8 Mar 2026 23:34:27 +0100 Subject: [PATCH 29/29] examples/animate: tweak rocket movement, add audio - fix duplicate symbol in sprites.s/pcmaudio.s --- examples/animate.pas | 62 ++++++++++++++++++++++++++++++++++---------- examples/sprites.s | 6 ++--- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/examples/animate.pas b/examples/animate.pas index f262c46..1ed379e 100644 --- a/examples/animate.pas +++ b/examples/animate.pas @@ -1,5 +1,5 @@ program animate; -uses sprites; +uses sprites,pcmaudio; type PictData = record magic,mode:integer; @@ -29,8 +29,33 @@ var pic:PictData; rocket2:Sprite; rocket3:Sprite; + buf:SndBufPtr; + + procedure WaitVSync; external; +function readAudioFile(fname:string):SndBufPtr; +var i,size:integer; + c:char; + buf:SndBufPtr; + f:file; +begin + open(f, fname, ModeReadOnly); + size := FileSize(f); + new(buf, size); + + buf^ := ''; + write('Reading ', size, ' bytes from ', fname); + for i := 1 to size do + begin + read(f,c); + AppendChar(buf^,c); + end; + writeln; + close(f); + readAudioFile := buf; +end; + procedure loadPalette(var pic:PictData); var i:integer; begin @@ -87,10 +112,13 @@ begin if aSprite.x > 608 then aSprite.x := 0; - if aSprite.y < 0 then + if aSprite.ydelta <> 0 then begin - aSprite.y := 200; - aSprite.x := 0; + if aSprite.y < 3 then + aSprite.ydelta := -aSprite.ydelta + else + if aSprite.y > 130 then + aSprite.ydelta := -aSprite.yDelta; end; end; @@ -106,7 +134,7 @@ var i:integer; r3oldX,r3oldY:integer; begin stickMan.x := 0; - stickMan.y := 322; + stickMan.y := 205; stickMan.frameTime := 6; stickMan.frameLeft := stickMan.frameTime; stickMan.curFrame := 0; @@ -116,30 +144,30 @@ begin stickman.moveLeft := stickMan.moveTime; rocket.x := 0; - rocket.y := 200; + rocket.y := 50; rocket.frameTime := 5; rocket.frameLeft := rocket.frameTime; rocket.curFrame := 0; rocket.xdelta := 3; - rocket.ydelta := -1; + rocket.ydelta := 1; rocket.moveTime := 1; rocket.moveLeft := rocket.moveTime; rocket2.x := 50; - rocket2.y := 100; + rocket2.y := 190; rocket2.frameTime := 5; rocket2.frameLeft := rocket2.frameTime; - rocket2.curFrame := 0; + rocket2.curFrame := 1; rocket2.xdelta := 3; - rocket2.ydelta := -1; + rocket2.ydelta := 0; rocket2.moveTime := 1; rocket2.moveLeft := rocket2.moveTime; rocket3.x :=100; - rocket3.y := 50; + rocket3.y := 90; rocket3.frameTime := 5; rocket3.frameLeft := rocket3.frameTime; - rocket3.curFrame := 0; + rocket3.curFrame := 2; rocket3.xdelta := 3; rocket3.ydelta := -1; rocket3.moveTime := 1; @@ -204,8 +232,14 @@ begin loadSpriteFrame(rocket, 3, infile, 3); close(infile); - rocket2 := rocket; rocket2.curFrame := 1; - rocket3 := rocket; rocket3.curFrame := 2; + rocket2 := rocket; + rocket3 := rocket; + buf := readAudioFile('footsteps.tdrau'); + SampleQStart(buf, true, 16000); + animLoop; + + SampleQStop; + dispose(buf); end. diff --git a/examples/sprites.s b/examples/sprites.s index 5f50081..01e2616 100644 --- a/examples/sprites.s +++ b/examples/sprites.s @@ -53,9 +53,9 @@ CALC_VMEM_ADDR: .EQU PS_SPILL 24 .EQU PS_STRIPE_C 28 .EQU PS_BPSAVE 32 - .EQU PS_FS 36 + .EQU PS_FS_ 36 PUTSPRITE: - FPADJ -PS_FS + FPADJ -PS_FS_ STORE PS_SPRITE_DATA STORE PS_Y STORE PS_X @@ -175,7 +175,7 @@ PS_L_XT: LOAD PS_BPSAVE STOREREG BP - FPADJ PS_FS + FPADJ PS_FS_ RET ; undraw a sprite, i.e. draw background data