Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions Millfork.sublime-syntax
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
%YAML 1.2
---
name: Millfork
scope: source.mfk
file_extensions: [mfk]

contexts:
main:
- include: comments
- include: preprocessor
- include: numbers
- include: strings
- include: keywords
- include: operators
- include: punctuation

comments:
- match: '//'
scope: punctuation.definition.comment.mfk
push:
- meta_scope: comment.line.double-slash.mfk
- match: $\n?
pop: true
- match: ';'
scope: punctuation.definition.comment.mfk
push:
- meta_scope: comment.line.semicolon.mfk
- match: $\n?
pop: true
- match: '/\*'
scope: punctuation.definition.comment.begin.mfk
push:
- meta_scope: comment.block.mfk
- match: '\*/'
scope: punctuation.definition.comment.end.mfk
pop: true

preprocessor:
# Poprawiona reguła obejmująca wszystkie warianty #elseif / #elsif / #else if
- match: '^\s*#\s*(if|else|else\s*if|elseif|elsif|endif|use|warn|error|info|fatal|pragma|infoeval|define)\b'
scope: keyword.control.preprocessor.mfk

numbers:
- match: '\b0[bB][01]+\b|\%[01]+\b'
scope: constant.numeric.binary.mfk
- match: '\b0[oOqQ][0-7]+\b'
scope: constant.numeric.octal.mfk
- match: '\b0[xX][0-9A-Fa-f]+\b|\$[0-9A-Fa-f]+\b|\b[0-9A-Fa-f]+[hH]\b'
scope: constant.numeric.hex.mfk
- match: '\b[0-9]+\b'
scope: constant.numeric.decimal.mfk
- match: ':-+|:\++'
scope: constant.language.unnamed-label.mfk

strings:
- match: '"'
scope: punctuation.definition.string.begin.mfk
push:
- meta_scope: string.quoted.double.mfk
- match: '\\.'
scope: constant.character.escape.mfk
- match: '"'
scope: punctuation.definition.string.end.mfk
pop: true

keywords:
# Typy danych
- match: '\b(void|bool|byte|sbyte|ubyte|word|farword|pointer|farpointer|long|word_be|word_le|long_be|long_le|file|int[0-9]+|signed[0-9]+|unsigned[0-9]+|set_carry|set_zero|set_overflow|set_negative|clear_carry|clear_zero|clear_overflow|clear_negative)\b'
scope: storage.type.mfk

# Modyfikatory
- match: '\b(const|volatile|register|static|stack|asm|inline|noinline|interrupt|kernal_interrupt|macro|reentrant|extern|segment|align|fast)\b'
scope: storage.modifier.mfk

# Kontrola przepływu
- match: '\b(if|else|for|while|do|until|to|downto|parallelto|paralleluntil|return|break|continue|goto|default)\b'
scope: keyword.control.mfk

# Inne słowa kluczowe
- match: '\b(import|array|struct|union|enum|alias|call|label)\b'
scope: keyword.other.mfk

# Wbudowane funkcje i stałe
- match: '\b(not|hi|lo|nonet|sizeof|sin|cos|tan|defined|true|false|nullptr|nullchar)\b'
scope: support.function.mfk

# Kodowania znaków
- match: '\b(default|defaultz|scr|scrz|petscii|ascii|petscr|pet|atascii|atari|bbc|sinclair|apple2|jis|jisx|iso_de|iso_yu|iso_no|iso_dk|iso_se|iso_fi|petsciiz|asciiz|petscrz|petz|atasciiz|atariz|bbcz|sinclairz|apple2z|jisz|jisxz|iso_dez|iso_yuz|iso_noz|iso_dkz|iso_sez|iso_fiz|utf8|utf16le|utf16be|latin0|latin9|iso8859_15|zx80|zx81|vectrex|koi7n2|short_koi|msx_intl|msx_us|msx_uk|msx_de|msx_fr|msx_es|msx_ru|msx_jp|msx_br|utf8z|utf16lez|utf16bez|latin0z|latin9z|iso8859_15z|zx80z|zx81z|vectrexz|koi7n2z|short_koiz|msx_intlz|msx_usz|msx_ukz|msx_dez|msx_frz|msx_esz|msx_ruz|msx_jpz|msx_brz)\b'
scope: constant.language.mfk

operators:
# Operatory specyficzne Millforka
- match: '(\$\+|\$\-|\$\*|\$\<{2}|\$\>{2}|\$\+\=|\$\-\=|\$\*\=|\$\<{2}\=|\$\>{2}\=)'
scope: keyword.operator.millfork.special
- match: '(\+''|\-''|\*''|\<{2}''|\>{2}''|\+''\=|\-''\=|\*''\=|\<{2}''\=|\>{2}''\=)'
scope: keyword.operator.millfork.special

# Operatory asemblera
- match: '#'
scope: keyword.operators.immediate.mfk
- match: '\.(?i:mod|bitnot|bitand|bitor|shl|shr|and|or|not|xor)\b'
scope: keyword.operators.word.mfk

# Standardowe operatory
- match: '(\+|\-|\*|/|%|<{2}|>{2}|==|!=|<=|>=|<|>|=|\+=|-=|\*=|/=|&&|&|\|{2}|\||\^|\^=)'
scope: keyword.operator.mfk

punctuation:
- match: '(\{|\}|\(|\)|\[|\]|@|#|;|,|\.)'
scope: punctuation.separator.mfk
Binary file added examples/x65/data/image.4bpp
Binary file not shown.
171 changes: 171 additions & 0 deletions examples/x65/mode1_4bpp.mfk
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
//-----------------------------------------------------------------------------
// MODE1 4bpp demo picture for X65 https://x65.zone/
//
// Displays a full-screen 384×240 paletted bitmap using CGIA MODE1.
// Each pixel is 4 bits, packing 2 pixels per byte, giving 16 effective
// colours (8 palette registers × half-bright flag).
//
// The display list follows the ANTIC-style approach: one mode-row
// instruction per raster line, generated at compile time by a `for` loop.
//-----------------------------------------------------------------------------

// Screen layout constants
const word BITMAP_ADDR = $2000 // where the bitmap lives in RAM
const byte BYTES_PER_LINE = 192 // 384 pixels / 2 pixels per byte (4bpp)
const byte ROWS = 240 // visible raster lines

// Load the packed 4bpp image file (46080 bytes)
array BITMAP @ BITMAP_ADDR = file("data/image.4bpp", 0)

//-----------------------------------------------------------------------------
// Global zero‑page variables
//-----------------------------------------------------------------------------

volatile int24 vblclock @ $0 // 24‑bit frame counter, incremented each VBL

byte b @ $3 // scratch byte
word w @ $4 // scratch word
pointer p @ $6 // scratch pointer

//-----------------------------------------------------------------------------
// Display List
//-----------------------------------------------------------------------------
// The CGIA is a fetch master — once configured, it reads the display list
// from memory every frame and drives the raster output without CPU help.
//
// This list:
// 1. LOAD_MEMORY sets the LMS (Memory Scan) pointer to the bitmap.
// 2. 240 × MODE1 instructions each draw a single raster line from the
// current LMS address, then advance LMS by `stride` bytes.
// 3. JUMP + DLI loops the display list back to the start after each
// vertical blank, so the same picture is redrawn every frame.
//-----------------------------------------------------------------------------

array(byte) dl align(fast) = [
DL_INS_LOAD_MEMORY | DL_INS_LM_MEMORY_SCAN, // load LMS pointer
@word[BITMAP_ADDR], // 16‑bit address of bitmap
for x, 1, until, ROWS [ DL_MODE_PALETTE_BITMAP ], // one MODE1 row per raster line
DL_INS_JUMP | DL_INS_DL_INTERRUPT, @word[dl.addr] // jump to DL start at VBL
]

//-----------------------------------------------------------------------------
// System initialisation
//-----------------------------------------------------------------------------
// Sets up the 65C816 CPU state, configures CGIA plane 0 for 4bpp bitmap
// mode, loads the palette, and enables vertical‑blank NMI interrupts so
// the `vblclock` counter runs.
//-----------------------------------------------------------------------------

macro void x65_init() {
// --- CPU housekeeping ---
asm {
sei ; disable maskable IRQs during setup
cld ; ensure binary arithmetic (not BCD)

ldx #$ff
txs ; initialise stack pointer to $01FF
}

// --- Disable all planes while we configure ---
cgia.planes = 0

// --- Clear all 16 plane‑0 registers to known state ---
p = cgia.plane0.addr
for b, 9, downto, 0 { p[b] = 0 }

// --- Configure plane 0 as 4bpp paletted bitmap ---
cgia.plane0.bckgnd.flags = PLANE_BITS_4BPP // 4 bits per pixel + half‑bright
cgia.plane0.bckgnd.row_height = 0 // each DL mode row = 1 raster line

// --- Palette (8 base colours; upper 8 are half‑bright variants) ---
// The CGIA uses a 256‑colour palette organised as 32 hues,
// each with 8 brightness steps (0 = darkest, 7 = brightest).
// In 4 bpp mode the low 3 bits of a pixel select one of these
// 8 registers; the high bit (half‑bright) flips to the opposite
// brightness half of the same hue row, giving 16 effective colours.
//
// Register Index Palette entry RGB approximation
// ----------------------------------------------------------
cgia.plane0.bckgnd.shared_color0 = 4 // hue 0, luma 4 → mid grey (146,146,146)
cgia.plane0.bckgnd.shared_color1 = 23 // hue 2, luma 7 → light orange (250,211,187)
cgia.plane0.bckgnd.color2 = 2 // hue 0, luma 2 → dark grey ( 73, 73, 73)
cgia.plane0.bckgnd.color3 = 3 // hue 0, luma 3 → grey (109,109,109)
cgia.plane0.bckgnd.color4 = 1 // hue 0, luma 1 → almost black ( 36, 36, 36)
cgia.plane0.bckgnd.color5 = 1 // hue 0, luma 1 → (duplicate)
cgia.plane0.bckgnd.color6 = 0 // hue 0, luma 0 → pure black ( 0, 0, 0)
cgia.plane0.bckgnd.color7 = 0 // hue 0, luma 0 → (duplicate)
//
// Half‑bright pairs for the above:
// index 4 ↔ index 196 (dark half of grey row)
// index 23 ↔ index 215 (dark half of orange row)
// index 2 ↔ index 194 (bright half of grey row)
// etc.

// --- Point plane 0 to the display list ---
cgia.offset0 = dl.addr

// --- Activate plane 0 as a background graphics plane (type = 0) ---
cgia.planes = 1

// --- Enable vertical‑blank NMI and reset frame counter ---
vblclock = 0
cgia.int_enable = %10000000 // VBI flag set → NMI at each VBL
}

//-----------------------------------------------------------------------------
// Vertical Blank Interrupt (NMI)
//-----------------------------------------------------------------------------
// Fires once per frame (60 Hz on NTSC‑like timing). Increments a 24‑bit
// software clock that can be used by the main loop for synchronising
// animations, delays, or timed events.
//-----------------------------------------------------------------------------

interrupt asm void vbl() {
.vblclock: inc vblclock.b2 ; increment low byte
bne .tickend ; if it wrapped to zero…
inc vblclock.b1 ; carry into middle byte
bne .tickend
inc vblclock.b0 ; carry into high byte
.tickend:
stz cgia.int_status ; acknowledge the interrupt (write‑1‑to‑clear)
rti
}

//-----------------------------------------------------------------------------
// Frame‑accurate delay helpers
//-----------------------------------------------------------------------------
// `pause()` waits exactly one frame (the VBL clock must advance).
// `wait(f)` waits `f` frames (0–255) and returns.
// Useful for simple synchronisation without a dedicated timer.
//-----------------------------------------------------------------------------

asm void pause() {
lda vblclock.b2 ; remember low byte of frame counter
.rt_check: cmp vblclock.b2 ; spin until it changes (next VBL)
beq .rt_check
rts
}

noinline asm void wait(byte register(a) f) {
clc
adc vblclock.b2 ; target = current low byte + f
.rt_check: cmp vblclock.b2 ; spin until low byte matches target
bne .rt_check
rts
}

//-----------------------------------------------------------------------------
// Main program
//-----------------------------------------------------------------------------
// The CGIA runs autonomously from the display list; the CPU is free.
// The infinite loop here does nothing, but real programs would place
// game logic or animation updates inside it.
//-----------------------------------------------------------------------------

void main() {
x65_init()

while true {
// wait(20) would pause here for 20 frames, etc.
}
}
Loading