Repository navigation
Releases
Releases · zstarczali/UltimateBasic
UltimateBasic 1.6.7
What's Changed
- Fix debugger source lines when subs come before the main program by @zstarczali in #68
- Add palette statement for Ultimate 64 / C64U colour palette by @zstarczali in #69
- ci: add manual workflow to publish packages to FTP by @zstarczali in #70
- ci: add optional GitHub release step to package workflow by @zstarczali in #71
Full Changelog: 1.6.6...1.6.7
UltimateBasic 64 1.6.6
ltimate Basic v1.6.6 – Release Notes
2026-10-18 – Unreleased (after 1.6.6)
New
- option strings n [, m]: a computed string variable holds up to n
characters (1-255, default 80), a string-array element up to m
(default 31). Each variable keeps its own buffer. - Compile-time warnings: a byte variable that gets a constant it
cannot hold (b = 300, b = a + 1000) is reported with its line.
Warnings never stop the build. - With --basic-print, STR$ gives the C64 BASIC text for every number
(" 7", "-42", " 3.5"); without it a byte keeps "007". - Struct types: field defaults (var hp: word = 100 - the array's
contents at program start), nested struct fields (e[i].pos.x),
fixed-length string fields (var name: string * 20), struct
parameters (sub heal(p AS TEntity) ... heal(e[i])), and the QBasic
form TYPE ... name AS type ... END TYPE. A struct element can be at
most 256 bytes (a compile error says so).
Changed - In a SUB / FUNCTION closed by END SUB / END FUNCTION, END alone on
its line ends the program, as in QBasic (also inside IF ... END IF).
Blocks in such routines are closed QBasic-style.
Fixed - / with a WORD, INTEGER or negative divisor used only the low byte
of the divisor: -700 / -7 gave 0, 50000 / (v + 0) was wrong. It is
a 16-bit division now, truncated toward zero. - END and BYE inside a sub or function returned to the caller
instead of ending the program. - A variable index into a struct array was multiplied in 8 bits, so
elements beyond byte offset 255 were read and written wrongly. - Word struct fields were cut to 8 bits inside expressions, and
word + x / word - x dropped the high byte of a 16-bit x (a word
array element, a word function).
2026-10-17 – Release 1.6.6: PRINT USING, string order, floats
New
- PRINT USING "format"; values (QBasic subset): numeric fields with
digits, . point, , thousands separators, a leading + (always a
sign) and a trailing - (sign after the number); a number that
does not fit is printed with a % in front. String fields & (whole
string), ! (first character) and \ \ (as wide as the field);
_ prints the next character as it is. Values cycle through the
fields; a trailing ; or , keeps the cursor on the line. The format
must be a string literal. - Strings can be ordered: < > <= >= compare in PETSCII order, a
shorter string first when it starts the other ("AB" < "ABC"). - RND as a floating-point number: in a statement that computes in
floating point, rnd / rnd() is 0 <= x < 1 (d = rnd, x = rnd() * 6).
rnd(n) and integer statements keep the 0-255 / 0..n-1 generator. - MOD with a SINGLE / DOUBLE operand works in floating point, like
VB / C fmod: 7.5 mod 2 = 1.5, -7.5 mod 2 = -1.5; x mod 0 = x. - Floating point in IRQ / NMI handlers: a handler sub (irq, nmi,
cia_timer) that uses SINGLE / DOUBLE saves and restores the
runtime state, so it no longer disturbs the main program. Handlers
without floating point cost nothing extra. - --basic-print (or a line "option basic print" in the program):
C64 BASIC PRINT spacing - numbers print with a leading space (or
their minus sign) and a trailing space (print 5 gives " 5 "), a ,
between items moves to the next 10-column zone, a trailing , keeps
the line. Without it the output is unchanged.
Changed - Programs carry only the floating-point routines they use: a small
program with + * and PRINT went from 4603 to 2293 bytes,
examples/float_demo.ub from 6319 to 5660. - MOD with a WORD or INTEGER operand is a real 16-bit remainder with
the sign of the dividend, like QBasic: -7 mod 3 = -1, 7 mod -3 = 1,
(w + 5) mod 7 is no longer cut to 8 bits. - SHR of an INTEGER is an arithmetic shift: -8 shr 1 = -4. WORD and
byte SHR stay logical.
Fixed - A literal from 32768 to 65535 in a floating-point statement was
read as a negative 16-bit pattern: d = 40000 gave -25536. It keeps
its value now; integer statements are unchanged.
UltimateBasic 64 1.6.5
Ultimate Basic v1.6.5 – Release Notes
2026-10-17 – Release 1.6.5: 16-bit address expressions
Fixed
- An address made of a 16-bit constant plus a byte variable was
computed in 8 bits: peek($07F8 + k) read $00F8 + k, and
poke $07F8 + k, v wrote into zero page. Addresses are now 16-bit
in peek, poke, peek16, poke16, fill, memcopy, drawmem, save and
reu (also the fill / memcopy length, e.g. 40 * rows).
constant + byte compiles to LDA / STA $07F8,Y. - poke ptr + i, v with a word pointer adds in 16 bits.
- peek(k) with a byte variable returned k itself; it now reads
memory at $00kk. - poke16 addr, base + k wrote only the low byte of the value.
UltimateBasic 64 1.6.2
Ultimate Basic v1.6.2 – Release Notes
2026-10-04 – Release 1.6.2: tracker instruments
New (tune ... end)
- imod id, vibdelay, vibspeed, vibdepth, pwmspeed, pwmmin, pwmmax:
instrument vibrato (triangle LFO after a delay, depth scaled to the
note's semitone) and pulse width modulation between min and max. - ifilt id, cutend, sweep, pingpong: filter cutoff sweep from the inst
cutoff toward cutend, sweep per frame, optionally back and forth. - igate id, gatetimer, hardrestart, firstwave: gate released gatetimer
frames before the next note, hard restart also zeroes ADSR, firstwave
is written on a note's first frame ($09 = test + gate). - itab id, speed, loop, "WW +n" / ".. -n" / "WW =n": wave / arpeggio
table (up to 32 steps), one step every speed frames, loop step or 255. - inst ctrl may carry ring modulation ($04) / sync ($02); modevol bit 7
= voice 3 off.
Changed - The player now works every frame (tables, vibrato, PWM, gate timer,
filter sweep). Filter routing is per voice: the voice that starts a
filter-routed instrument owns cutoff / resonance / mode, $D417 routes
every voice whose instrument is filter-routed. A tune blob is about
2.5 KB of player code plus its data. - Same algorithm as the Visual Assembler SID editor's player; the new
tests/tune_player.rs compares the SID registers frame by frame with
a trace of the editor's engine.
2026-10-03 – Release 1.6.1: smooth scrolling maps
New
- map view px, py: shows the loaded map at a pixel position, once per
frame (instead of wait raster). XSCROLL / YSCROLL with 38 columns /
24 rows, two screen buffers ($0400 / $3C00) swapped at char steps,
the next char cell built two frames ahead, color RAM copied in two
halves behind / ahead of the raster beam: smooth and tear-free in
every direction. Sprite pointers are copied to both buffers. - map load reads a VisualAssembler Level Editor project (.bin) and
composes its screens into one map (max 6x10 screens, 255x255 cells). - map load "file", addr: the map data goes to a fixed address (like
incbin "file", addr) instead of inline in the code. - Compile errors: map view without map load, maps smaller than 40x25,
program code reaching into $3C00-$3FFF. - Example: examples/level_scroll_demo.ub.
Fixes
- A byte product or shift assigned to a word variable inside a sum
was truncated to 8 bits (py = cy * 8 + fy gave 67 instead of 323
for cy = 40, fy = 3); it is now computed in 16 bits, like a lone
product. w = b shl n keeps 16 bits too. - A constant multiplier above 255 was cut to 8 bits (cy * 300); the
16-bit operand now always takes the multiplicand side.
UltimateBasic 64 1.6.0
Ultimate Basic v1.6.0 – Release Notes
2026-10-03 – Release 1.6.0: optimised 6502 code generation
Performance
- Conditions (if / while / until / select) compile to CMP + branch
instead of materialising a 0/1 value and testing it again
(if x == 10: 24 -> 9 bytes).x > nbecomesx >= n+1; and / or of
comparisons short-circuit when the right side is side-effect free;
while loops are rotated (test at the bottom); select compares the
variable in place. - Constants and byte variables are used directly as operands
(ADC #n, CMP zp) instead of being staged in scratch zero page; poke to a
constant or word address stores directly; identity operations vanish. *by a constant: shifts / shift-add (x * 8 -> 3x ASL); otherwise a
shift-add loop of at most 8 rounds (the old repeated-addition loop
needed ~16 cycles per unit of the multiplier)./andmodby a power of two: LSR / AND #(n-1); otherwise an 8-round
shift-subtract loop (the old loop ran once per unit of the quotient).- x = x +/- 1|2 -> INC / DEC (word: 16-bit INC / DEC for +/-1).
- Arrays with a variable index use base,Y addressing instead of a
zero-page pointer. - for loops are rotated (no JMP per iteration); constant limits / steps
are immediates and use no zero page. - A-register reuse: the leading LDA of a statement is skipped when it
reloads the value the previous statement just stored.
Fixes
- Division by zero used to hang: now x / 0 = 255 and x mod 0 = x.
for i = 0 to 255looped forever; the step now detects the 8-bit
wrap-around in both directions, so the loop runs 256 times and stops.
Tests
- Full 6502 emulator for tests (tests/common/cpu6502.rs), semantic tests
that run compiled programs (tests/codegen_semantics.rs), code-shape /
cycle tests (tests/codegen_opt.rs) and an ignored differential fuzzer
against a reference compiler (tests/differential_fuzz.rs).
2026-09-26 – chain: load and start another program
New language elements
- Added
chain "FILE" [, device]: loads another program to its own load
address (normally $0801, right over the running program) and RUNs it, as
if LOAD"FILE",8,1 and RUN had been typed. For boot / title programs.
A 46-byte position-independent loader and the file name (1-16
characters) are copied to the cassette buffer $033C. Before the load the
I/O is reset (VIC IRQs off, SID volume 0, IOINIT, RESTOR, $01 = $37);
after it the end-of-program pointer $2D/$2E is set, BASIC is
re-initialised ($E453, $E3BF - a self-extracting cruncher such as
Exomizer sfx may have used BASIC's zero page) and the program is RUN
($A659, $A7AE). Without a device the drive the program came from ($BA)
is used, else drive 8. If the file cannot be loaded, the program
continues with the statement afterchain.
Fixes
load: the secondary address was swapped.load "FILE"used secondary
0 with X/Y = 0 and loaded the file to $0000;load "FILE", addrused
secondary 1 and ignoredaddr. Nowload "FILE"loads to the file's own
address (secondary 1) andload "FILE", addrtoaddr(secondary 0).--d64: directory entries were written 30 bytes apart instead of 32, so
every file after the first had a broken entry (e.g.--add data.prg
showed up as a garbled name and could not be loaded). Also fixed: disk ID
and DOS type were at the wrong BAM offsets; file data only used tracks
1-17 (a larger disk crashed the build instead of reporting it); sectors
are now allocated like the 1541 DOS does (tracks 17->1, then 19->35,
interleave 10). A full disk is reported as "D64 error: disk full ...".
New tests: tests/d64_tests.rs reads the image back (BAM, directory,
sector chains) and compares every file.
2026-09-25 – Initialised arrays
New language elements
- Added
data <array>: v, v, ...: fills a declaredarray/array_word
with constant values. Further lines continue where the previous one
stopped; word arrays take 16-bit values (stored lo, hi). The values are
stored after the code and copied into the array once at program start
(right after the arrays are zeroed), so the table is writable and indexed
like any other array (sinus[i]). These lines do not affectread.
An undeclared name or more values than the array holds is a compile-time
error. This is what the Visual Assembler Curve Editor's "Export to
UltimateBasic" and "Export UltimateBasic demo" generate.
Ultimate Basic v1.5.7
Ultimate Basic v1.5.7 – Release Notes
2026-09-21 – Tracker tunes and an inline-assembler fix
New language elements
- Added
tune ... end: describe a 3-voice SID tracker tune (instruments,
order list, per-voice pattern rows such as "C-4 01 V24") directly in the
source. The compiler builds a self-contained, zero-page-free player at
compile time and definessid_init/sid_play, somusic play|stop| pause|resumework exactly as withload sid. Vibrato, slide, note cut
and speed change are supported.tune at $addrmoves the blob (default
$8000). This is what the Visual Assembler SID editor's "Export to
UltimateBasic" now generates.
Fixes
- Inline
asm { }:label,X,label,YandJMP (label)were assembled
as plain absolute instructions (the index/indirect mode was dropped), so
table lookups such asLDA table,Xread the wrong byte. The addressing
mode is now kept. music play: the CIA1 IRQ wrapper acknowledged the timer by WRITING $01 to
$DC0D, which clears the interrupt mask, so the music IRQ stopped after the
first tick. It now acknowledges by reading $DC0D.
2026-09-21 – Custom charsets, code layout and non-blocking sound
New language elements
- Added
charset on/charset off.charset addr(default $3800) used to
only set the destination ofchardef;charset onnow points the VIC-II
at that RAM set (read-modify-write of $D018 bits 1-3, the screen-matrix
bits are kept) andcharset offrestores the ROM set ($1000, or $1800
afterlowercase). The base must be a multiple of $800 inside VIC bank 0. - Added a build-time check that generated code does not overlap the charset:
charset onclaims the 2 KB set and everychardefits 8 bytes; a
program that reaches into them stops with "the program code (...)
overlaps the charset area ...". Data in anorggap is not counted. - Added
org addr. Placed between sub/fn definitions it continues the
subroutine code ataddrand zero-fills the gap, which stays free for
data (fonts, sprites, tables). Subs never fall through, so this is safe;
orgbelow the current code end is an error. - Extended
incbinwithincbin "file", addr, skip [, len]: skip a header
(e.g. a .prg load address) and/or take only a slice.incbin ..., addrcan
now be written into anorggap. - Added
sfx channel, freq, frames [, wave]: a non-blocking SID note. It
sets attack 0 / sustain 0 / release 0 and picks the decay fromframes
(1 frame = 20 ms) at compile time, re-triggers the voice and returns at
once.waveis 16 (triangle), 32 (saw, default), 64 (pulse) or 128
(noise). Unlikesoundit is safe inside a game loop.
Fixes
soundwaited on raster line 0, which $D012 (low 8 bits) shows twice per
frame (lines 0 and 256), so notes were half as long as requested; two of
its branches were also patched over their opcode, crashing the program
for any non-zero duration. It now waits on raster line 200 likedelay.wordassignments computed products in 8 bits:w += 300 * level,
w = w + 100 * levelandw = 100 * level + 900now keep all 16 bits.print a / 10(and other arithmetic on afloat) printed the raw Q8.8
value as an integer; it now printsN.DD.- Arrays are zeroed once at program start (RAM contents at power-up are
random;array(N)previously started with whatever was there). - Running out of permanent zero page ($02-$4F) is now a compile-time
error ("out of zero page"). Before, variables andforlimit/step
bytes silently spilled into the scratch area and broke loops. Sequential
forloops now release their two limit/step bytes when the loop ends
(if the body declared no variables), so many loops no longer exhaust it. - Manual corrections:
%binary literals were documented but never
supported by the lexer (use $hex or decimal); thecharsetexample used
the $D018 value of another address ($1E selects $3800, $1A selects $2800). - Tests: a missing
ROL zpopcode in the test CPU was added, and the koala
tests no longer race on a shared temp file.
UltimateBasic 64 1.5.6
Ultimate Basic v1.5.6 – Release Notes
2026-09-06 – Hires draw color + multicolor shapes
- Added
color pen c: sets a persistent hires drawing color (0-15). The
plot/line/rect/circle/paint helpers stamp it into the video-matrix
foreground nibble of every cell they touch; the background nibble is
preserved. Default is white (1), so programs that never callcolor pen
render exactly as before. Emitted only whencolor penis used, so the
plot helper is unchanged for existing programs. - Added multicolor shape commands
mline,mrect, andmcircle(joining
mplot). The trailingcolorargument is the 2-bit source (0-3); the
actual colors come from the cell palette (screen / color RAM), exactly as
formplot. They reuse the hires Bresenham/midpoint routines via a shared
draw-target switch, plotting each pixel through the mplot helper.mline
andmrectrequire x in 0-159;mcircleclips off-screen points.
UltimateBasic 64 1.5.5
Ultimate Basic v1.5.5 – Release Notes
2026-08-26 – Magic Desk CRT export
- Added
.crtoutput support: when the output filename ends in.crt, the
compiler now writes a Commodore 64 cartridge image instead of a raw PRG. - The cartridge uses the same banked Magic Desk type-19 layout as
VisualAssembler: 64-byte header, bank 0 boot loader, and the compiled PRG
payload copied into RAM on startup. - Existing
.prgand.d64output paths are unchanged. - Added integration tests covering the CRT header, bank layout, payload copy,
and boot stub.
2026-08-18 – Struct types (type ... endtype)
- New user-defined struct types with
type NAME ... endtypeblocks. Fields
are declared inside asvar name: int,var name: word, orvar name: float.
Element size is the sum of field widths (no padding).
type TEntity
var x: int
var y: int
var hp: word # 1 + 1 + 2 = 4 bytes per element
endtype - Array-of-struct declarations look like regular arrays:
var enemies: TEntity = array(N) ; allocates N * elem_size bytes at $C000+ - Field access on both sides of assignment:
enemies[0].x = 5 ; constant idx → STA absolute
enemies[i].hp = enemies[i].hp + 1 ; variable idx → shift-add mul + (ptr),Y - Constant-index writes/reads fold to bare LDA/STA absolute. Variable-index
access emits a small shift-and-add multiplier for idx * elem_size (uses
ASL A chains for power-of-two sizes, or a shift-add sequence for arbitrary
sizes via two scratch zero-page bytes), then indexed (ptr),Y access. - Word and float fields write both bytes (little-endian): STA addr / STA addr+1
for constant idx, STA (ptr),Y ; INY ; STA (ptr),Y for variable idx. - Float fields use Q8.8 fixed-point format (hi byte = integer, lo byte =
fraction × 256). Reading a float field into afloatvariable works via
gen_word_assign; printing a float struct field uses print_fixed ("N.DD"). - The parser resolves
arr[i].fieldinto Stmt::StructSet and Expr::StructGet
nodes that carry (field_offset, field_kind, elem_size) — codegen is
self-contained and needs no type table lookup. - Not yet implemented in this release: struct-typed sub/fn parameters,
default field values (parsed for compatibility with the proposal but not
initialized at allocation time), nested struct fields, multi-dimensional
struct arrays, string fields. Errors reported: struct-typed var without
array(N) initializer, and unknown-field access. - Added
examples/type_demo.uband CPU-run integration tests for constant
and variable index, int, word, and float field kinds, and the unknown-field
error.
2026-08-18 – Count-down for-loop fix
for/loopwith a negative constantstep(e.g.for i = 20 to 0 step -2)
used to compile but never enter the body, and — for values that wrap past 0 —
loop forever. The exit test was unsigned-only, so counting down from 20 to 0
kept re-entering after the 254 (= -2 mod 256) wrap-around.- The compiler now inspects the sign of a constant
stepat code generation:- positive step or default +1 → BCC/BEQ to body, unchanged (exit when var > to)
- negative constant step → BCS to body (exit when var < to), plus a
post-incrementBCS loop_top ; JMP exitthat catches the ADC underflow
(carry clear) when var wraps below 0. This is what keeps
for i = N to 0 step -kfinite.
- A default-step
for/loopwherefrom > to(e.g.loop i = 10 to 1, no
step) used to silently skip the body — 0 iterations, no diagnostic. It is
now a compile-time error:
"for-loop: from (10) > to (1) with default step +1 loops 0 times
— use 'step -1' to count down" - Non-constant
step(variable or expression) is still treated as positive at
compile time. Use awhileloop when you need count-down with a runtime step. - Added
examples/countdown_demo.ub(working forms) and
examples/countdown_errors_demo.ub(the compile-time error form). - Added integration tests: a codegen check that count-down loops emit
BCS
for the body branch, a check that count-up loops still emitBCC+BEQ,
should_paniccoverage of the from>to default-step error, and CPU-run tests
that execute the loop and verify it terminates with the expected iteration count.
2026-08-18 – --explicit CLI flag
- Added the
--explicitbuild flag:
ub build myprog.ub --explicit
With the flag, everyvar,sub-parameter, andfn-parameter declaration
must carry a:typeannotation. Untyped declarations become compile-time
errors:
line 12: 'explicit' mode: 'var loose' has no type
— use 'var loose: int|word|float|string'
line 14: 'explicit' mode: parameter 'a' has no type
— use 'a: int|word|float|string' - Array declarations (
array(N),array_word(N)) andconst NAME = value
are unaffected — their type is fixed by the declaration form. - The flag is CLI-only; no source-level directive keyword. Existing programs
compile unchanged without the flag. Add it to a Makefile / build script to
enforce typed style project-wide. - Added
examples/explicit_demo.ub(annotated code that builds under the flag)
andexamples/explicit_errors_demo.ub(loose code that builds without the
flag but reports errors with it). - Added integration tests covering the typed-ok case, untyped var / sub-param /
fn-param errors, and the without-flag regression.
UltimateBasic 64 1.5.3
Ultimate Basic v1.5.3 – Release Notes
2026-08-18 – Multi-dimensional arrays
- Arrays can now be declared with more than one dimension using a
comma-separated dimension list, for both byte and word arrays:
var grid = array(8, 8) # 8x8 = 64 bytes
var wm = array_word(4, 4) # 4x4 word elements = 32 bytes - Elements are indexed with a comma-separated subscript list and stored
row-major; the flat offset is base + i0*(d1*..) + i1*(d2*..) + ... :
grid[row, col] = 42
var v = grid[row, col] - Any number of dimensions is supported. Dimensions must be compile-time
constants (integer literals orconstvalues). - When every subscript is a constant the address is folded at compile time to a
single absolute store/load; a variable subscript emits the row*stride + col
computation followed by an indexed (ptr),Y access. - A single subscript into a multi-dimensional array is still allowed and is
treated as a flat/linear index (e.g. grid[10]). - Indexing with a subscript count that is neither 1 nor the declared rank is
reported as a compile-time error. - Implemented entirely in the parser (multi-index folds to one flat index
expression), so code generation is unchanged. - Added examples/array2d_demo.ub and integration tests covering constant and
variable indices, row-major layout, word-element stride, linear access,
const-derived dimensions, and the wrong-rank error.
2026-08-18 – PETSCII bracket encoding fix
- Fixed string encoding of the '[' , ']' and '^' characters, which previously
fell through to the unknown-character placeholder and printed as '?'. - They now emit their correct C64 codes ($5B, $5D, $5E). These code points are
identical in the uppercase/graphics and lowercase charsets, so the fix is
correct in bothuppercaseandlowercasemodes. - Added a regression test for bracket encoding in both charset modes.
UltimateBasic 64 1.5.2
Ultimate Basic v1.5.2 – Release Notes
2026-08-15 – Readable assembly codegen listings
- Added the
--asmbuild option:
ub build demo.ub --asm
A successful build writesdemo.asmnext to the selected.prgoutput. - The listing is generated from compiler metadata rather than being only a bare
disassembly of the completed PRG. - UB statement boundaries are retained as comments alongside their emitted code.
- Zero-page variables, arrays, subroutines, BASIC labels, relative branches, and
in-program JMP/JSR targets receive readable symbols or generated labels. - Each instruction includes its final C64 address and exact machine-code bytes.
- Compiler-generated helpers are included in final memory order, while unknown
opcodes are preserved as.bytedirectives. - Helper entry points now use descriptive
ub_helper_*labels. - Known data regions (DATA, maps, sprite/character definitions, lookup tables,
incbins, SID and Koala payloads) are emitted as named.byteblocks instead of
being disassembled as instructions; long zero gaps use.fill. - Output uses reassemblable KickAssembler syntax, including
.labelconstants and
a valid* = originprogram counter directive. - Added automated listing tests and full README.md, MANUAL.md, CLI-help, and
developer-guide documentation.
Ultimate Basic v1.5.1 – Release Notes
2026-08-15 – Koala Painter image import
- Added compile-time loading of standard and raw Koala Painter images:
koala load "picture.kla"
koala show
koala hide - Added file, memory-layout, and SID-conflict diagnostics plus emulation-backed
tests and full manual documentation.
2026-08-15 – Character tile maps
- Added compile-time loading and validation of self-describing
.ubmapfiles:
map load "level.ubmap"
VisualAssemblerme-map.binexports are also accepted directly, without an
.ubmapconversion step. - Added 40x25 viewport rendering from arbitrary map origins:
map draw map_x, map_y
Character codes go to $0400 and optional per-cell colors go to $D800. - Added writable runtime access with
map_tile(),map_color(),map set, and
map color, allowing game logic and destructible maps without reloading data. - Added normal and multicolor character-map modes. Multicolor maps configure
$D016 MCM and the three shared colors in $D021-$D023. - Documented the byte-exact UBMP v1 format, flags, row-major arrays, viewport
constraints, and charset relationship in MANUAL.md and CLAUDE.md. - Added emulation-backed tests for normal/multicolor setup, screen and color-RAM
rendering, tile/color mutation and queries, and invalid-file diagnostics.
2026-08-12 – Debug symbol exports
- Added side-effect-free software bounding-box collision detection:
var hit = box_hit(l1, t1, r1, b1, l2, t2, r2, b2)
It returns 1 for overlapping or edge-touching inclusive AABBs and 0 otherwise,
without reading or clearing the VIC-II $D01E/$D01F collision registers. - Extended
sprite_framewith an optional animation frame expression:
sprite_frame id, base_addr, frame
Consecutive frames occupy 64-byte slots; the original two-argument syntax is
unchanged and remains backward compatible. - Added emulation-backed integration tests for overlap, separation, touching edges,
and runtime animation-frame pointer selection. - Added the
--debugbuild option:
ub build demo.ub --debug
A successful build now writes three debugger files next to the.prg, using
the output file's stem. - Added KickAssembler-compatible
.symexport. It can be imported by assembler
source and contains resolved symbols in.label name = $addressform. - Added C64Debugger/RetroDebugger
.dbgexport in KickAssembler debug-dump XML
format. It contains the UltimateBasic program segment, source filename, and
resolved address labels. - Added VICE
.vsmonitor-command export. It containsal C:address .label
commands and can be loaded with VICE's-moncommandsoption orllcommand. - Debug exports include
program_start,program_end, zero-page variables,
arrays, subroutines, and BASIC labels. - BASIC labels are now retained with their final machine-code addresses in the
compiler memory map, making them available to all debug exporters. - Symbol names are sanitized for debugger compatibility, and source paths are
XML-escaped in.dbgoutput. - Added unit coverage for all three symbol syntaxes and documented the new CLI
option and file formats in README.md, MANUAL.md, and CLAUDE.md. - Current limitation:
.dbgprovides segments and address symbols, but does not
yet contain instruction-to-source-line mappings for source-level stepping.