From 6a40f23578cf0ab8b73257c98d151c0b94a4cc32 Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Fri, 24 Nov 2023 21:26:34 +0100 Subject: [PATCH] cx16: added bmx library module and showbmx example --- compiler/res/prog8lib/cx16/bmx.p8 | 337 ++++++++++++++++++++++++ compiler/test/TestCompilerOnExamples.kt | 1 + docs/source/libraries.rst | 23 ++ examples/cx16/showbmx.p8 | 47 ++++ examples/test.p8 | 48 ++-- parser/antlr/Prog8ANTLR.g4 | 1 - 6 files changed, 441 insertions(+), 16 deletions(-) create mode 100644 compiler/res/prog8lib/cx16/bmx.p8 create mode 100644 examples/cx16/showbmx.p8 diff --git a/compiler/res/prog8lib/cx16/bmx.p8 b/compiler/res/prog8lib/cx16/bmx.p8 new file mode 100644 index 000000000..016c4f100 --- /dev/null +++ b/compiler/res/prog8lib/cx16/bmx.p8 @@ -0,0 +1,337 @@ +; Routines to load and save "BMX" files (commander X16 bitmap format) Version 1. +; Only uncompressed images are supported for now. +; BMX Specification: https://cx16forum.com/forum/viewtopic.php?t=6945 +; TODO: make read_palette() and write_palette() use a palette_buffer_ptr to store palette into system ram instead of directly into vram. + +%import diskio + +bmx { + + ubyte[32] header + str FILEID = petscii:"bmx" + + ubyte bitsperpixel ; consider using set_bpp() when setting this + ubyte vera_colordepth ; consider using set_vera_colordepth() when setting this + uword width + uword height + ubyte border + ubyte palette_entries ; 0 means 256, all of them + ubyte palette_start + ubyte compression + + uword error_message ; pointer to error message, or 0 if all ok + uword max_width = 0 ; should you want load() to check for this + uword max_height = 0 ; should you want load() to check for this + + sub load(ubyte drivenumber, str filename, ubyte vbank, uword vaddr, uword screen_width) -> bool { + ; Loads a BMX bitmap image and palette into vram. (and Header info into the bmx.* variables) + ; Parameters: + ; the drive number and filename to load, + ; the vram bank and address where the bitmap data should go, + ; and the width of the current screen mode (can be 0 if you know no padding is needed). + ; You can set the max_width and max_height variables first, if you want this routine to check those. + ; Note: does not change vera screen mode or colordepth! You have to do that yourself! + ; Returns: success status. If false, error_message points to the error message string. + error_message = 0 + ubyte old_drivenumber = diskio.drivenumber + diskio.drivenumber = drivenumber + if diskio.f_open(filename) { + diskio.reset_read_channel() + if read_header() { + if parse_header() { + if max_width and width>max_width { + error_message = "image too large" + goto load_end + } + if screen_width and width>screen_width { + error_message = "image too large" + goto load_end + } + if max_height and height>max_height { + error_message = "image too large" + goto load_end + } + if compression { + error_message = "compression not supported" + goto load_end + } + if read_palette() { + if not read_bitmap(vbank, vaddr, screen_width) + error_message = "bitmap error" + } else + error_message = "palette error" + } else + error_message = "invalid bmx file" + } else + error_message = "invalid bmx file" + } else + error_message = diskio.status() + +load_end: + diskio.f_close() + diskio.drivenumber = old_drivenumber + return error_message==0 + } + + sub load_header(ubyte drivenumber, str filename) -> bool { + ; Loads just the header data from a BMX bitmap image into the bmx.* variables. + ; Parameters: the drive number and filename to load. + ; Returns: success status. If false, error_message points to the error message string. + error_message = 0 + ubyte old_drivenumber = diskio.drivenumber + diskio.drivenumber = drivenumber + if diskio.f_open(filename) { + diskio.reset_read_channel() + if read_header() { + if not parse_header() + error_message = "invalid bmx file" + } else + error_message = "invalid bmx file" + } else + error_message = diskio.status() + +load_end: + diskio.f_close() + diskio.drivenumber = old_drivenumber + return error_message==0 + } + + sub load_palette(ubyte drivenumber, str filename) -> bool { + ; Loads just the palette from a BMX bitmap image into vram. (and Header info into the bmx.* variables) + ; Parameters: the drive number and filename to load. + ; Returns: success status. If false, error_message points to the error message string. + error_message = 0 + ubyte old_drivenumber = diskio.drivenumber + diskio.drivenumber = drivenumber + if diskio.f_open(filename) { + diskio.reset_read_channel() + if read_header() { + if parse_header() { + if not read_palette() + error_message = "palette error" + } else + error_message = "invalid bmx file" + } else + error_message = "invalid bmx file" + } else + error_message = diskio.status() + +load_end: + diskio.f_close() + diskio.drivenumber = old_drivenumber + return error_message==0 + } + + sub save(ubyte drivenumber, str filename, ubyte vbank, uword vaddr, uword screen_width) -> bool { + ; Save bitmap and palette data from vram into a BMX file. + ; First you must have set all bmx.* variables to the correct values! (like width, height..) + ; Parameters: + ; drive number and filename to save to, + ; vram bank and address of the bitmap data to save, + ; and the width of the current screen mode (or 0 if you know no padding is needed). + ; Returns: success status. If false, error_message points to the error message string. + if compression { + error_message = "compression not supported" + return false + } + error_message = 0 + ubyte old_drivenumber = diskio.drivenumber + diskio.drivenumber = drivenumber + if diskio.f_open_w(filename) { + cx16.r0 = diskio.status() + if cx16.r0[0]!='0' { + error_message = cx16.r0 + goto save_end + } + diskio.reset_write_channel() + if screen_width + width = min(width, screen_width) + if write_header() { + if write_palette() { + if not write_bitmap(vbank, vaddr, screen_width) + error_message = "bitmap error" + } else + error_message = "palette error" + } else + error_message = "header error" + } else + error_message = diskio.status() +save_end: + diskio.f_close_w() + diskio.drivenumber = old_drivenumber + return error_message==0 + } + + sub set_bpp(ubyte bpp) { + bitsperpixel = bpp + vera_colordepth = 0 + when bpp { + 2 -> vera_colordepth = 1 + 4 -> vera_colordepth = 2 + 8 -> vera_colordepth = 3 + } + } + + sub set_vera_colordepth(ubyte depth) { + vera_colordepth = depth + bitsperpixel = 1 << depth + } + +; ------------------- helper routines, usually not needed to call yourself ------------------------- + + sub read_header() -> bool { + ; load a BMX header from the currently active input file + for cx16.r0L in 0 to sizeof(header)-1 { + header[cx16.r0L] = cbm.CHRIN() + } + return not cbm.READST() + } + + sub read_palette() -> bool { + ; load palette data from the currently active input file + cx16.vaddr(1, $fa00+palette_start*2, 0, 1) + cx16.r0L = palette_entries + do { + cx16.VERA_DATA0 = cbm.CHRIN() + cx16.VERA_DATA0 = cbm.CHRIN() + cx16.r0L-- + } until cx16.r0L==0 + return not cbm.READST() + } + + sub read_bitmap(ubyte vbank, uword vaddr, uword screenwidth) -> bool { + ; load contiguous bitmap into vram from the currently active input file + cx16.vaddr(vbank, vaddr, 0, 1) + cx16.r1 = bytes_per_scanline(width) + cx16.r2 = 0 + if width bool { + ; save out the BMX header to the currently active output file + build_header() + for cx16.r0L in 0 to sizeof(header)-1 { + cbm.CHROUT(header[cx16.r0L]) + } + return not cbm.READST() + } + + sub write_palette() -> bool { + ; save full palette straight out of vram to the currently active output file + cx16.vaddr(1, $fa00+palette_start*2, 0, 1) + cx16.r0L = palette_entries + do { + cbm.CHROUT(cx16.VERA_DATA0) + cbm.CHROUT(cx16.VERA_DATA0) + cx16.r0L-- + } until cx16.r0L==0 + return not cbm.READST() + } + + sub write_bitmap(ubyte vbank, uword vaddr, uword screenwidth) -> bool { + ; save contiguous bitmap from vram to the currently active output file + cx16.vaddr(vbank, vaddr, 0, 1) + cx16.r1 = bytes_per_scanline(width) + cx16.r2 = 0 + if width uword { + when bitsperpixel { + 1 -> cx16.r0L = 3 + 2 -> cx16.r0L = 2 + 4 -> cx16.r0L = 1 + 8 -> return w + else -> return 0 + } + return w >> cx16.r0L + } + + sub parse_header() -> bool { + if header[0]==FILEID[0] and header[1]==FILEID[1] and header[2]==FILEID[2] { + if header[3]==1 { ; only version 1 supported for now + bitsperpixel = header[4] + vera_colordepth = header[5] + width = peekw(&header+6) + height = peekw(&header+8) + palette_entries = header[10] + palette_start = header[11] + ; Data start is not needed + compression = header[14] + border = header[15] + return true + } + } + return false + } + + sub build_header() { + ; build the internal BMX header structure + ; normally you don't have to call this yourself + sys.memset(header, sizeof(header), 0) + uword data_offset = sizeof(header) + palette_entries*2 + header[0] = FILEID[0] + header[1] = FILEID[1] + header[2] = FILEID[2] + header[3] = 1 ; version 1 + header[4] = bitsperpixel + header[5] = vera_colordepth + header[6] = lsb(width) + header[7] = msb(width) + header[8] = lsb(height) + header[9] = msb(height) + header[10] = palette_entries + header[11] = palette_start + header[12] = lsb(data_offset) + header[13] = msb(data_offset) + header[14] = compression + header[15] = border + } +} diff --git a/compiler/test/TestCompilerOnExamples.kt b/compiler/test/TestCompilerOnExamples.kt index 2955266f5..2b4577092 100644 --- a/compiler/test/TestCompilerOnExamples.kt +++ b/compiler/test/TestCompilerOnExamples.kt @@ -121,6 +121,7 @@ class TestCompilerOnExamplesCx16: FunSpec({ "multi-irq-new", "plasma", "rasterbars", + "showbmx", "snow", "spotlight", "tehtriz", diff --git a/docs/source/libraries.rst b/docs/source/libraries.rst index 300ffab2a..f4ec3efed 100644 --- a/docs/source/libraries.rst +++ b/docs/source/libraries.rst @@ -477,6 +477,29 @@ On the other targets, it only contains the definition of the 16 memory mapped vi ``restore_virtual_registers()`` restore the values of all 16 virtual registers r0 - r15 from the buffer. Might be useful in an IRQ handler to avoid clobbering them. +bmx (cx16 only) +---------------- +Routines to load and save "BMX" files, the CommanderX16 bitmap file format. +Specification available here: https://cx16forum.com/forum/viewtopic.php?t=6945 +Only *uncompressed* bitmaps are supported in this library. + +The routines are designed to be fast and bulk load/save the data directly into or from vram, +without the need to buffer something in main memory. + +Here is the load routine: + +``load(ubyte drivenumber, str filename, ubyte vbank, uword vaddr, uword screen_width)`` + Loads a BMX bitmap image and palette into vram. (and Header info into the bmx.* variables) + Parameters: + the drive number and filename to load, + the vram bank and address where the bitmap data should go, + and the width of the current screen mode (can be 0 if you know no padding is needed). + You can set the ``bmx.max_width`` and ``bmx.max_height`` variables first, if you want this routine to check those. + Returns: success status. If false, ``bmx.error_message`` points to the error message string. + +For details about the other routines such as ``save``, have a look at +the `source code `_ . + emudbg (cx16 only) ------------------- diff --git a/examples/cx16/showbmx.p8 b/examples/cx16/showbmx.p8 new file mode 100644 index 000000000..d9020d5af --- /dev/null +++ b/examples/cx16/showbmx.p8 @@ -0,0 +1,47 @@ + +; viewer program for BMX image files. +; see https://cx16forum.com/forum/viewtopic.php?t=6945 + +%import textio +%import bmx +%option no_sysinit +%zeropage basicsafe + +main { + sub start() { + str filename = "?"*40 + + repeat { + txt.print("\nenter bmx image filename: ") + if txt.input_chars(&filename) { + if bmx.load_header(8, filename) { + txt.print("\nwidth: ") + txt.print_uw(bmx.width) + txt.print("\nheight: ") + txt.print_uw(bmx.height) + txt.print("\nbpp: ") + txt.print_uw(bmx.bitsperpixel) + txt.nl() + sys.wait(100) + + ; switch to correct screen mode and color depth + void cx16.screen_mode($80, false) + cx16.VERA_L0_CONFIG = cx16.VERA_L0_CONFIG & %11111100 | bmx.vera_colordepth + ; actually load + if bmx.load(8, filename, 0, 0, 320) { + void txt.waitkey() + } + } + + cbm.CINT() ; reset screen + + if bmx.error_message { + txt.print("load error:\n") + txt.print(bmx.error_message) + txt.nl() + sys.wait(120) + } + } + } + } +} diff --git a/examples/test.p8 b/examples/test.p8 index 2a6238b42..04bbbb1bc 100644 --- a/examples/test.p8 +++ b/examples/test.p8 @@ -1,25 +1,43 @@ %import textio -%import syslib +%import bmx +%option no_sysinit %zeropage basicsafe + main { sub start() { - ubyte xx = 1 - uword ww=1 + str filename = "?"*40 + repeat { + txt.print("\nenter bmx image filename: ") + if txt.input_chars(&filename) { + if bmx.load_header(8, filename) { + txt.print("\nwidth: ") + txt.print_uw(bmx.width) + txt.print("\nheight: ") + txt.print_uw(bmx.height) + txt.print("\nbpp: ") + txt.print_uw(bmx.bitsperpixel) + txt.nl() + sys.wait(100) - xx |= %0001000 - txt.print_ubbin(xx, true) - txt.nl() - xx &= %11110111 - txt.print_ubbin(xx, true) - txt.nl() + ; switch to correct screen mode and color depth + void cx16.screen_mode($80, false) + cx16.VERA_L0_CONFIG = cx16.VERA_L0_CONFIG & %11111100 | bmx.vera_colordepth + ; actually load + if bmx.load(8, filename, 0, 0, 320) { + void txt.waitkey() + } + } + cbm.CINT() ; reset screen - ww |= %0001000 - txt.print_uwbin(ww, true) - txt.nl() - ww &= %11110111 - txt.print_uwbin(ww, true) - txt.nl() + if bmx.error_message { + txt.print("load error:\n") + txt.print(bmx.error_message) + txt.nl() + sys.wait(120) + } + } + } } } diff --git a/parser/antlr/Prog8ANTLR.g4 b/parser/antlr/Prog8ANTLR.g4 index b26e01594..b9ca853ef 100644 --- a/parser/antlr/Prog8ANTLR.g4 +++ b/parser/antlr/Prog8ANTLR.g4 @@ -188,7 +188,6 @@ expression : arrayindexed: scoped_identifier arrayindex - // TODO to allow chained/multidimensional array indexing: | arrayindexed arrayindex ;