diff --git a/CHANGELOG.md b/CHANGELOG.md index aa10817e..6398e6d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ See [extension/CHANGELOG.md](extension/CHANGELOG.md). ### Debugging and the `8bws` CLI - New `8bws` command-line tool (`npm run cli`) to build and run programs headlessly: `build`, `run`, `verify-replay`, `profile`, `detect`, `list-platforms`, `list-tools`; scripted emulator control; `--png`, `--json`, `--symbols`, `--memdump`, `--info`; platform and main file detected from a project directory. +- Platform detection no longer confuses the Atari 8-bit targets: `atari.h` means the 800 (it is guarded by `__ATARI__`), `atari5200.h` means the 5200, and a folder named for a platform breaks a near-tie. - Emulators now run on a timeline: step back, seek, reverse run, `rbreak`, and deterministic replay with a replay-verification test that found determinism bugs in several platforms. - Step Over for C and assembly sources, a Breakpoints pane, and more breakpoint conditions. - Call Stack view renamed Call Graph, with clocks and more detail per line. @@ -16,6 +17,7 @@ See [extension/CHANGELOG.md](extension/CHANGELOG.md). - Click the hex offset in the gutter to run to that line. - Waveform viewer for Verilog, with help and shortcuts. - Faster line lookups in listings. +- Atari 8-bit: the XEX loader now calls each INIT vector before loading the next chunk (cc65's system-check chunk overlaps the main program), fixing a crash on startup of `tgidemo.c`. ### Editor and UI @@ -41,7 +43,7 @@ See [extension/CHANGELOG.md](extension/CHANGELOG.md). - **Game Boy**: native emulator with GBC double speed, RTC and banking; C via a partial gbdk-2020 header set; presets ported from GBDK, plus Chase and Paku Paku; ghosting on the LCD. - **Atari 8-bit**: `.atr` disk images, 5th player, ANTIC and POKEY fixes, a8lib common library, scrolling and libdemo examples, oscar64 support. - **Atari 2600**: a Javatari-derived `vcs.jt4` machine that also runs headlessly; updated `vcs.h`/`macro.h`. -- **Apple II**: paddle/joystick and SW0-2 support, dynamic origin, DOS 3.3 and AppleSingle samples. +- **Apple II**: paddle/joystick and SW0-2 support, dynamic origin, DOS 3.3 and AppleSingle samples. New self-contained hi-res TGI driver (`a2hires.s`, 280x192, 8 colors, no ROM calls) used by the TGI demos. - **NES**: updated jsnes and a fix for a write bug. - **SMS/Game Gear/ColecoVision**: shared code, SMS Solarian port, more presets. - **Arcade**: Pac-Man hardware, Williams, Galaxian and Astrocade accuracy updates; early Exidy, MCR and Channel F work. @@ -57,6 +59,10 @@ See [extension/CHANGELOG.md](extension/CHANGELOG.md). - New build directives replace the legacy CFGFILE-style ones; build parameters no longer leak between builds. - `#pragma compile("...")` and oscar64 symbol/listing parsing. - Tool metadata centralized in `src/common/toolmeta.ts`; `npm run toolversions` checks tool versions. +- cc65/ca65/ld65 warnings no longer fail the build; only errors are reported (the WASI tools print non-fatal `Warning:` lines such as duplicate `const` qualifiers). +- cc65 toolchain (cc65/ca65/ld65 WASI builds and the per-platform include/lib zips) updated from the 2021 build (`6ac4aa4`) to cc65 `d8a486a` (V2.19-3867, 2026-09-26). The compiler runs with `--disable-opt OptLoadStore1` (the new step drops stores after loads of the same address, which breaks hardware registers and inline asm) and `-unreachable-code`. +- cc65 `a2.lo.s` (Apple II lo-res TGI) patched in `8bitworkshop-compilers/patches/cc65-apple2-lo-tgi.patch`: a stray `bpl :+` in `INSTALL` made `tgi_install(a2_lo_tgi)` crash on a non-enhanced Apple II. +- Presets adjusted for the newer cc65: NES `chase` closes its zero-page `#pragma` region, C64 `side_scroller` uses an unsigned fixed-point constant, and the Atari vector presets no longer recurse into `main()`. `neslib.h` no longer redefines `NULL` if `stddef.h` already did. - Build is now only `esbuild` through `scripts/build.mjs`; code split into a headless `src/common/projectcore.ts` / `toolselect.ts`. - New `buildpresets` script that builds and runs every preset. diff --git a/presets/apple2/a2hires.h b/presets/apple2/a2hires.h new file mode 100644 index 00000000..e6da3a30 --- /dev/null +++ b/presets/apple2/a2hires.h @@ -0,0 +1,2 @@ +/* Self-contained hi-res TGI driver (280x192, 8 colors); see a2hires.s */ +extern unsigned char a2hires_tgi[]; diff --git a/presets/apple2/a2hires.s b/presets/apple2/a2hires.s new file mode 100644 index 00000000..f264abc6 --- /dev/null +++ b/presets/apple2/a2hires.s @@ -0,0 +1,499 @@ +; +; Self-contained TGI driver for the Apple ][ hi-res screen (280x192, 8 colors) +; +; The stock cc65 hi-res driver calls Applesoft ROM routines, which the +; emulator's ROM doesn't have. This one draws everything itself. +; Usage: //#link "a2hires.s" then tgi_install(a2hires_tgi); +; (no text output, no palette changes) +; +; The picture lives on hi-res page 2 ($4000-$5FFF), so programs may use +; everything below $4000 (the default config puts code at $0803 up). +; + .include "zeropage.inc" + .include "tgi-kernel.inc" + .include "tgi-error.inc" + + .export _a2hires_tgi + +; Soft switches +TXTCLR := $C050 +TXTSET := $C051 +MIXCLR := $C052 +MIXSET := $C053 ; MIXCLR+1 +LOWSCR := $C054 +HISCR := $C055 ; LOWSCR+1 +LORES := $C056 +HIRES := $C057 +WNDTOP := $22 + +; Parameters passed by the TGI kernel +X1 := ptr1 +Y1 := ptr2 +X2 := ptr3 +Y2 := ptr4 + +; Scratch (never live across a driver call) +SCR := sreg ; pointer to screen byte +MASK := tmp1 ; pixel mask within byte +TMP := tmp2 + +; ------------------------------------------------------------------------ + + .rodata + +; Driver header: signature, capabilities, then the jump table. +; Entries are absolute since this driver is linked statically. +_a2hires_tgi: + .byte $74, $67, $69 ; "tgi" + .byte TGI_API_VERSION + .addr $0000 ; Library reference + .word 280 ; X resolution + .word 192 ; Y resolution + .byte 8 ; Number of drawing colors + .byte 1 ; Number of screens + .byte 7 ; System font X size + .byte 8 ; System font Y size + .word $00EA ; Aspect ratio (based on 4/3 display) + .byte 0 ; TGI driver flags + + .addr INSTALL + .addr UNINSTALL + .addr INIT + .addr DONE + .addr GETERROR + .addr CONTROL + .addr CLEAR + .addr SETVIEWPAGE + .addr SETDRAWPAGE + .addr SETCOLOR + .addr SETPALETTE + .addr GETPALETTE + .addr GETDEFPALETTE + .addr SETPIXEL + .addr GETPIXEL + .addr LINE + .addr BAR + .addr TEXTSTYLE + .addr OUTTEXT + +; Colors: 0 black, 1 green, 2 violet, 3 white, 4 black2, 5 orange, 6 blue, 7 white2 +DEFPALETTE: + .byte 0, 1, 2, 3, 4, 5, 6, 7 + +; Per-color pixel behavior, indexed by color & 3 +EVENSET: .byte 0, 0, 1, 1 ; set the pixel in even columns? +ODDSET: .byte 0, 1, 0, 1 ; set the pixel in odd columns? + +ROWOFS: .byte 0, 40, 80 ; 40 bytes per row of 8 scanlines, 3 groups + +; ------------------------------------------------------------------------ + + .bss + +ERROR: .res 1 +SETEVEN: .res 1 ; current color: set pixel in even columns +SETODD: .res 1 ; set pixel in odd columns +PALBIT: .res 1 ; palette bit ($00 or $80) + +XDIV: .res 256 ; x / 7 +XMASK: .res 256 ; 1 << (x % 7) +ROWLO: .res 192 ; address of scanline y on page 1 +ROWHI: .res 192 + +; Pixel position, also used as the running point of LINE +PX: .res 2 +PY: .res 1 + +; Line state +SXDIR: .res 1 ; $00 = x increases, $FF = decreases +SYDIR: .res 1 +MAJVEC: .res 2 ; routine that steps the major axis +MINVEC: .res 2 ; ... and the minor axis +LEN: .res 2 ; major axis length (steps remaining) +TOT: .res 2 ; copy of LEN +MINOR: .res 2 ; minor axis length +ERR: .res 2 + +; ------------------------------------------------------------------------ + + .code + +INSTALL: +UNINSTALL: +SETVIEWPAGE: +SETDRAWPAGE: +TEXTSTYLE: +OUTTEXT: + rts + +; INIT: build lookup tables, then switch on page 1 of hi-res +INIT: + ; x -> byte offset and bit mask, for x = 0..255 + ldx #0 + stx tmp3 ; div + stx tmp4 ; mod + lda #1 + sta MASK +@xloop: lda tmp3 + sta XDIV,x + lda MASK + sta XMASK,x + asl MASK + inc tmp4 + lda tmp4 + cmp #7 + bcc @nx + lda #0 + sta tmp4 + lda #1 + sta MASK + inc tmp3 +@nx: inx + bne @xloop + + ; y -> address of scanline: $2000 + (y&7)*$400 + ((y>>3)&7)*$80 + (y>>6)*40 + ldy #0 +@yloop: tya + and #7 + asl + asl + ora #$20 + sta TMP + tya + lsr + lsr + lsr + and #7 + lsr ; carry = bit 0 of the 8-line group + ora TMP + sta TMP + lda #0 + ror ; $80 if carry + sta MASK + tya + asl + rol + rol + and #3 + tax + lda ROWOFS,x + clc + adc MASK + sta ROWLO,y + lda TMP + adc #0 + sta ROWHI,y + iny + cpy #192 + bne @yloop + + bit MIXCLR + bit HISCR + bit HIRES + bit TXTCLR + lda #TGI_ERR_OK + sta ERROR + rts + +DONE: + bit TXTSET + bit LOWSCR + bit LORES + lda #0 + sta WNDTOP + rts + +GETERROR: + lda ERROR + ldx #TGI_ERR_OK + stx ERROR + rts + +; CONTROL: data = 0 for full screen, 1 for 4 lines of text +CONTROL: + ora ptr1+1 + bne @err + lda ptr1 + cmp #2 + bcs @err + tax + beq @top + lda #20 +@top: sta WNDTOP + lda MIXCLR,x + lda #TGI_ERR_OK + .byte $2C ; BIT absolute +@err: lda #TGI_ERR_INV_ARG + sta ERROR + rts + +; CLEAR: fill the screen with zeros +CLEAR: + lda #0 + sta SCR + lda #$40 + sta SCR+1 + ldx #32 ; 32 pages of 256 bytes + ldy #0 + tya +@loop: sta (SCR),y + iny + bne @loop + inc SCR+1 + dex + bne @loop + rts + +; SETCOLOR: A = 0..7 +SETCOLOR: + tax + and #3 + tay + lda EVENSET,y + sta SETEVEN + lda ODDSET,y + sta SETODD + txa + and #4 + beq @lo + lda #$80 +@lo: sta PALBIT + rts + +SETPALETTE: + lda #TGI_ERR_INV_FUNC + sta ERROR + rts + +GETPALETTE: +GETDEFPALETTE: + lda #DEFPALETTE + rts + +; ADDR: locate the pixel at PX/PY. Returns the byte address in (SCR),y +; and the pixel's bit in MASK. +ADDR: + ldx PY + lda ROWLO,x + sta SCR + lda ROWHI,x + clc + adc #$20 ; page 2 + sta SCR+1 + ldy #0 ; byte offset of the 256 column page + ldx PX + lda PX+1 + beq @lo + txa ; x >= 256: use x-252, which is 36 bytes on + sec + sbc #252 + tax + ldy #36 +@lo: tya + clc + adc XDIV,x + tay + lda XMASK,x + sta MASK + rts + +; PLOT: draw the pixel at PX/PY in the current color +PLOT: + jsr ADDR + ldx SETEVEN + lda PX + lsr ; carry = odd column + bcc @even + ldx SETODD +@even: lda MASK + ora #$80 ; the palette bit is always rewritten + eor #$FF + and (SCR),y + ora PALBIT + cpx #0 + beq @store + ora MASK +@store: sta (SCR),y + rts + +SETPIXEL: + lda X1 + sta PX + lda X1+1 + sta PX+1 + lda Y1 + sta PY + jmp PLOT + +; GETPIXEL: returns black/white (+4 for the second palette) +GETPIXEL: + lda X1 + sta PX + lda X1+1 + sta PX+1 + lda Y1 + sta PY + jsr ADDR + lda (SCR),y + tax ; remember the palette bit + and MASK + beq @black + lda #3 +@black: cpx #$80 + bcc @done + adc #3 ; carry is set: +4 +@done: ldx #0 + rts + +; LINE: DDA between X1/Y1 and X2/Y2. The major axis is stepped every pixel, +; and the minor axis whenever the accumulated error passes the major length. +LINE: + lda X1 + sta PX + lda X1+1 + sta PX+1 + lda Y1 + sta PY + + ; dx = |x2 - x1| + lda #0 + sta SXDIR + sec + lda X2 + sbc X1 + sta LEN + lda X2+1 + sbc X1+1 + sta LEN+1 + bcs @dxpos + dec SXDIR ; $FF + sec ; negate + lda #0 + sbc LEN + sta LEN + lda #0 + sbc LEN+1 + sta LEN+1 +@dxpos: + ; dy = |y2 - y1| + lda #0 + sta SYDIR + sta MINOR+1 + sec + lda Y2 + sbc Y1 + bcs @dypos + dec SYDIR + eor #$FF + adc #1 +@dypos: sta MINOR + + ; the major axis is the longer one + lda MINOR + cmp LEN + lda MINOR+1 + sbc LEN+1 + bcs @ymajor ; dy >= dx + lda #stepx + sta MAJVEC+1 + lda #stepy + sta MINVEC+1 + bne @go ; branch always +@ymajor: + lda LEN ; swap: LEN <-> MINOR + ldx MINOR + sta MINOR + stx LEN + lda LEN+1 + ldx MINOR+1 + sta MINOR+1 + stx LEN+1 + lda #stepy + sta MAJVEC+1 + lda #stepx + sta MINVEC+1 +@go: lda LEN + sta TOT + lda LEN+1 + sta TOT+1 + lsr ; error starts at half the length + sta ERR+1 + lda LEN + ror + sta ERR +@loop: jsr PLOT + lda LEN + ora LEN+1 + beq @done + lda LEN + bne @nb + dec LEN+1 +@nb: dec LEN + jsr @major + clc + lda ERR + adc MINOR + sta ERR + lda ERR+1 + adc MINOR+1 + sta ERR+1 + lda ERR ; error >= length? + cmp TOT + lda ERR+1 + sbc TOT+1 + bcc @loop + sec ; yes: subtract it and step the minor axis + lda ERR + sbc TOT + sta ERR + lda ERR+1 + sbc TOT+1 + sta ERR+1 + jsr @minor + jmp @loop +@done: rts +@major: jmp (MAJVEC) +@minor: jmp (MINVEC) + +stepx: lda SXDIR + bne @dec + inc PX + bne @ret + inc PX+1 +@ret: rts +@dec: lda PX + bne @nb + dec PX+1 +@nb: dec PX + rts + +stepy: lda SYDIR + bne @dec + inc PY + rts +@dec: dec PY + rts + +; BAR: horizontal lines from Y1 to Y2 (the kernel sorts and clips the corners) +BAR: + inc Y2 +@row: lda Y2 + pha + lda Y1 + sta Y2 + jsr LINE + pla + sta Y2 + inc Y1 + cmp Y1 + bne @row + rts diff --git a/presets/apple2/icepiano.c b/presets/apple2/icepiano.c new file mode 100644 index 00000000..a08c8565 --- /dev/null +++ b/presets/apple2/icepiano.c @@ -0,0 +1,165 @@ +/* +Hi-res (280x192, 6 colors) TGI demo for the Apple ][. +Four full-screen animations; press a key to switch to the next (ESC quits): + a Lissajous line figure, a swirl of dots, random lines, random bars. +Each shape is remembered in a ring buffer; when the buffer wraps, the +oldest shape is erased, so the picture keeps moving. +Uses our own hi-res TGI driver, a2hires.s (the stock one needs +Applesoft ROM routines). apple2-hgr2.cfg keeps $2000-$5FFF free. +*/ +#include +#include +#include +#include +#include "a2hires.h" +//#link "a2hires.s" +//#tooldef ld cfgfile=apple2-hgr2.cfg +//#resource "apple2-hgr2.cfg" + +#define W 280 // screen size +#define H 192 +#define R (H/2 - 4) // radius of the figures +#define MAXSEG 240 + +// hi-res colors: 1 green, 2 violet, 5 orange, 6 blue +static const unsigned char Colors[4] = { 1, 2, 5, 6 }; + +#define SIN(a) _sin((unsigned)(a) % 360) +#define COS(a) _sin(((unsigned)(a) + 90) % 360) + +typedef struct { + int x1, y1, x2, y2; + unsigned char color; +} Seg; + +static Seg ring[MAXSEG]; // the shapes currently on screen +static unsigned head; // next slot to overwrite +static unsigned count; // slots in use +static unsigned T; // time, advances every step +static unsigned char mode; +static const unsigned ringsize[4] = { 40, 240, 70, 14 }; + +// scale a sine value (-256..256) to -R..R +static int scale(int v) +{ + return (int)(((long)v * R) / 256); +} + +static unsigned char randcolor(void) +{ + return Colors[rand() & 3]; +} + +// draw (or, with color 0, erase) one shape for the current mode +static void draw(const Seg* s, unsigned char color) +{ + int cx = W/2, cy = H/2; + tgi_setcolor(color); + switch (mode) { + case 0: // line, mirrored four ways + tgi_line(cx + s->x1, cy + s->y1, cx + s->x2, cy + s->y2); + tgi_line(cx - s->x1, cy + s->y1, cx - s->x2, cy + s->y2); + tgi_line(cx + s->x1, cy - s->y1, cx + s->x2, cy - s->y2); + tgi_line(cx - s->x1, cy - s->y1, cx - s->x2, cy - s->y2); + break; + case 1: // dot + tgi_setpixel(s->x1, s->y1); + break; + case 2: // line + tgi_line(s->x1, s->y1, s->x2, s->y2); + break; + case 3: // bar + tgi_bar(s->x1, s->y1, s->x2, s->y2); + break; + } +} + +// Lissajous lines +static void make_lissajous(Seg* s) +{ + unsigned a = T * 2; + s->x1 = scale(SIN(a * 3)) * 3/2; + s->y1 = scale(COS(a * 2)); + s->x2 = scale(COS(a * 5 + T)) * 3/2; + s->y2 = scale(SIN(a * 4)); + s->color = Colors[(T >> 4) & 3]; +} + +// dots on a wandering rose curve +static void make_swirl(Seg* s) +{ + unsigned a = T * 7; + int r = scale(SIN(a * 5 + T / 2)) + scale(COS(a * 3)) / 2; + s->x1 = W/2 + r * COS(a) / 128; + s->y1 = H/2 + r * SIN(a) / 256; + s->color = Colors[(a / 90) & 3]; +} + +// random horizontal and vertical lines +static void make_hatch(Seg* s) +{ + int x = rand() % W; + int y = rand() % H; + int len = 4 + rand() % 60; + s->x1 = s->x2 = x; + s->y1 = s->y2 = y; + if (rand() & 1) { + s->x2 = x + len < W ? x + len : W - 1; + } else { + s->y2 = y + len < H ? y + len : H - 1; + } + s->color = randcolor(); +} + +// random filled bars +static void make_bar(Seg* s) +{ + int x = rand() % (W - 8); + int y = rand() % (H - 4); + int w = 8 + rand() % (W/2); + int h = 4 + rand() % 24; + s->x1 = x; + s->y1 = y; + s->x2 = x + w < W ? x + w : W - 1; + s->y2 = y + h < H ? y + h : H - 1; + s->color = randcolor(); +} + +// add one shape, erasing the oldest if the ring is full +static void step(void) +{ + Seg* s = &ring[head]; + unsigned size = ringsize[mode]; + if (count == size) { + draw(s, TGI_COLOR_BLACK); + } else { + count++; + } + switch (mode) { + case 0: make_lissajous(s); break; + case 1: make_swirl(s); break; + case 2: make_hatch(s); break; + case 3: make_bar(s); break; + } + draw(s, s->color); + if (++head == size) head = 0; + T++; +} + +int main(void) +{ + tgi_install(a2hires_tgi); + tgi_init(); + for (;;) { + tgi_clear(); + head = count = T = 0; + while (!kbhit()) { + step(); + if (mode == 1) { step(); step(); step(); } + } + if (cgetc() == 27) break; + mode = (mode + 1) & 3; + } + tgi_uninstall(); + return 0; +} diff --git a/presets/apple2/tgidemo.c b/presets/apple2/tgidemo.c index ae6fdaf6..bf650a01 100644 --- a/presets/apple2/tgidemo.c +++ b/presets/apple2/tgidemo.c @@ -1,7 +1,8 @@ /* NOTE: The alternate Apple ][ ROM used by 8bitworkshop - is not compatible with the hi-res TGI driver. - So we use the lo-res driver here. + has no Applesoft routines, so the stock hi-res TGI driver + doesn't work. We use our own hi-res driver, a2hires.s. + apple2-hgr2.cfg keeps $2000-$5FFF free for the picture. */ #include #include @@ -10,6 +11,10 @@ NOTE: The alternate Apple ][ ROM used by 8bitworkshop #include #include #include +#include "a2hires.h" +//#link "a2hires.s" +//#tooldef ld cfgfile=apple2-hgr2.cfg +//#resource "apple2-hgr2.cfg" #ifndef DYN_DRV # define DYN_DRV 0 @@ -189,7 +194,7 @@ int main (void) CheckError ("tgi_load_driver"); #else /* Install the driver */ - tgi_install (a2_lo_tgi); + tgi_install (a2hires_tgi); CheckError ("tgi_install"); #endif diff --git a/presets/atari8-800/icepiano.c b/presets/atari8-800/icepiano.c new file mode 100644 index 00000000..e63b81d9 --- /dev/null +++ b/presets/atari8-800/icepiano.c @@ -0,0 +1,170 @@ +/* +Hi-res (280x192, 6 colors) TGI demo for the Apple ][. +Four full-screen animations; press a key to switch to the next (ESC quits): + a Lissajous line figure, a swirl of dots, random lines, random bars. +Each shape is remembered in a ring buffer; when the buffer wraps, the +oldest shape is erased, so the picture keeps moving. +Uses our own hi-res TGI driver, a2hires.s (the stock one needs +Applesoft ROM routines). apple2-hgr2.cfg keeps $2000-$5FFF free. +*/ +#include +#include +#include +#include + +//#tooldef ld cfgfile=atari.cfg +//#symbol ld __RESERVED_MEMORY__=8192 + +#define W 80 // screen size +#define H 192 +#define R (H/2 - 4) // radius of the figures +#define MAXSEG 240 +#define MAXCOL 7 + +#define SIN(a) _sin((unsigned)(a) % 360) +#define COS(a) _sin(((unsigned)(a) + 90) % 360) + +typedef struct { + int x1, y1, x2, y2; + unsigned char color; +} Seg; + +static Seg ring[MAXSEG]; // the shapes currently on screen +static unsigned head; // next slot to overwrite +static unsigned count; // slots in use +static unsigned T; // time, advances every step +static unsigned char mode; +static const unsigned ringsize[4] = { 40, 240, 70, 14 }; + +// scale a sine value (-256..256) to -R..R +static int scale(int v) +{ + return (int)(((long)v * R) / 256); +} + +static unsigned char randcolor(void) +{ + return rand() & MAXCOL; +} + +// draw (or, with color 0, erase) one shape for the current mode +static void draw(const Seg* s, unsigned char color) +{ + int cx = W/2, cy = H/2; + tgi_setcolor(color); + switch (mode) { + case 0: // line, mirrored four ways + tgi_line(cx + s->x1, cy + s->y1, cx + s->x2, cy + s->y2); + tgi_line(cx - s->x1, cy + s->y1, cx - s->x2, cy + s->y2); + tgi_line(cx + s->x1, cy - s->y1, cx + s->x2, cy - s->y2); + tgi_line(cx - s->x1, cy - s->y1, cx - s->x2, cy - s->y2); + break; + case 1: // dot + tgi_setpixel(s->x1, s->y1); + break; + case 2: // line + tgi_line(s->x1, s->y1, s->x2, s->y2); + break; + case 3: // bar + tgi_bar(s->x1, s->y1, s->x2, s->y2); + break; + } +} + +// Lissajous lines +static void make_lissajous(Seg* s) +{ + unsigned a = T * 2; + s->x1 = scale(SIN(a * 3)) * 3/2; + s->y1 = scale(COS(a * 2)); + s->x2 = scale(COS(a * 5 + T)) * 3/2; + s->y2 = scale(SIN(a * 4)); + s->color = (T >> 4) & MAXCOL; +} + +// dots on a wandering rose curve +static void make_swirl(Seg* s) +{ + unsigned a = T * 7; + int r = scale(SIN(a * 5 + T / 2)) + scale(COS(a * 3)) / 2; + s->x1 = W/2 + r * COS(a) / 128; + s->y1 = H/2 + r * SIN(a) / 256; + s->color = (a / 90) & MAXCOL; +} + +// random horizontal and vertical lines +static void make_hatch(Seg* s) +{ + int x = rand() % W; + int y = rand() % H; + int len = 4 + rand() % 60; + s->x1 = s->x2 = x; + s->y1 = s->y2 = y; + if (rand() & 1) { + s->x2 = x + len < W ? x + len : W - 1; + } else { + s->y2 = y + len < H ? y + len : H - 1; + } + s->color = randcolor(); +} + +// random filled bars +static void make_bar(Seg* s) +{ + int x = rand() % (W - 8); + int y = rand() % (H - 4); + int w = 8 + rand() % (W/2); + int h = 4 + rand() % 24; + s->x1 = x; + s->y1 = y; + s->x2 = x + w < W ? x + w : W - 1; + s->y2 = y + h < H ? y + h : H - 1; + s->color = randcolor(); +} + +// add one shape, erasing the oldest if the ring is full +static void step(void) +{ + Seg* s = &ring[head]; + unsigned size = ringsize[mode]; + if (count == size) { + draw(s, TGI_COLOR_BLACK); + } else { + count++; + } + switch (mode) { + case 0: make_lissajous(s); break; + case 1: make_swirl(s); break; + case 2: make_hatch(s); break; + case 3: make_bar(s); break; + } + draw(s, s->color); + if (++head == size) head = 0; + T++; +} + +void set_palette(void) { + int i; + for (i=0; i<=MAXCOL; i++) { + _setcolor(i, i*2, 15); + } +} + +int main(void) +{ + tgi_install(atr10_tgi); + tgi_init(); + set_palette(); + for (;;) { + tgi_clear(); + head = count = T = 0; + while (!kbhit()) { + step(); + if (mode == 1) { step(); step(); step(); } + } + if (cgetc() == 27) break; + mode = (mode + 1) & 3; + } + tgi_uninstall(); + return 0; +} diff --git a/presets/c64/side_scroller.c b/presets/c64/side_scroller.c index 0436343c..4cd5301f 100644 --- a/presets/c64/side_scroller.c +++ b/presets/c64/side_scroller.c @@ -71,7 +71,7 @@ const char SPRITE_DATA[64*NUM_SPRITE_PATTERNS] = { #define OBSTACLE_INDEX 2 #define CENTER_X 172 // sprite start X coord. -#define FLOOR_Y (128 << 8) // sprite start Y (fixed 8.8) +#define FLOOR_Y (128u << 8) // sprite start Y (fixed 8.8) #define JUMP_VELOCITY (-900) // jump velocity (fixed 8.8) #define GRAVITY 32 // gravity (fixed 8.8) diff --git a/presets/nes/chase/game.c b/presets/nes/chase/game.c index 4cdff5d6..8f969fc2 100644 --- a/presets/nes/chase/game.c +++ b/presets/nes/chase/game.c @@ -287,6 +287,12 @@ static unsigned char bright; static unsigned char update_list[7*3+1]; +// close the zero-page region. (Without the matching pop, cc65 V2.19-3867 also +// moves the map[] declared above into ZEROPAGE, overflowing the 256-byte area.) + +#pragma bss-name(pop) +#pragma data-name(pop) + //smoothly fade current bright to the given value diff --git a/presets/vector-ataricolor/game.c b/presets/vector-ataricolor/game.c index b84ef6eb..890fdd2f 100644 --- a/presets/vector-ataricolor/game.c +++ b/presets/vector-ataricolor/game.c @@ -376,22 +376,23 @@ byte just_one_actor_left() { } void main() { - dvgclear(); - dvgwrofs = 0x800; //write into the middle of VEC ROM (TODO) - make_cached_shapes(); - create_obstacles(5); - new_player_ship(); - while (!just_one_actor_left()) { - dvgreset(); - control_player(); - draw_and_update_actors(); - CNTR(); - HALT(); - dvgstart(); - remove_expired_actors(); - frame++; - watchdog=0; -// while (vidframe == (frame & 0x3)) {} + for (;;) { + dvgclear(); + dvgwrofs = 0x800; //write into the middle of VEC ROM (TODO) + make_cached_shapes(); + create_obstacles(5); + new_player_ship(); + while (!just_one_actor_left()) { + dvgreset(); + control_player(); + draw_and_update_actors(); + CNTR(); + HALT(); + dvgstart(); + remove_expired_actors(); + frame++; + watchdog=0; + // while (vidframe == (frame & 0x3)) {} + } } - main(); } diff --git a/presets/vector-ataricolor/skeleton.cc65 b/presets/vector-ataricolor/skeleton.cc65 index 5d8bf07b..bbd1bc82 100644 --- a/presets/vector-ataricolor/skeleton.cc65 +++ b/presets/vector-ataricolor/skeleton.cc65 @@ -17,5 +17,4 @@ void main() { HALT(); dvgstart(); } - main(); } diff --git a/src/common/detect.ts b/src/common/detect.ts index f3392b60..b028db6e 100644 --- a/src/common/detect.ts +++ b/src/common/detect.ts @@ -65,7 +65,7 @@ const SYSTEM_HEADERS: { [header: string]: string[] } = { 'cbm.h': ['c64', 'vic20'], 'apple2.h': ['apple2'], 'apple2enh.h': ['apple2'], - 'atari.h': ['atari8-800', 'atari8-5200'], + 'atari.h': ['atari8-800'], 'atari5200.h': ['atari8-5200'], 'atari7800.h': ['atari7800'], 'pce.h': ['pce'], @@ -292,7 +292,9 @@ export async function detectProject(input: DetectInput): Promise { export function isClearWinner(detections: Detection[]): boolean { if (!detections.length || detections[0].score < 0.5) return false; var w = (d: Detection) => d.weight ?? d.score; - return detections.length == 1 || w(detections[0]) - w(detections[1]) >= 0.2; + // weights are rounded for display, so allow for the rounding error when the + // lead lands exactly on the threshold (e.g. a 0.2 folder-name hint) + return detections.length == 1 || w(detections[0]) - w(detections[1]) >= 0.2 - 1e-9; } /** diff --git a/src/docs/cc65-upgrade.md b/src/docs/cc65-upgrade.md new file mode 100644 index 00000000..54e6dd35 --- /dev/null +++ b/src/docs/cc65-upgrade.md @@ -0,0 +1,39 @@ +# cc65 Toolchain Upgrade + +The cc65 family (`cc65`, `ca65`, `ld65`) used by the 6502 platforms was upgraded +for 8bitworkshop 4.0. + +| | Old build | Current build | +| --- | --- | --- | +| cc65 commit | `6ac4aa4` (2021-12-24) | `d8a486a` (V2.19-3867, 2026-09-26) | +| Runtime | Emscripten | WASI (`WASIRunner`) | +| Includes/libs | hand-mixed 2019–2023 objects | one set built from the same revision | +| Files | `fs65-*.data` (~22 MB) | `cc65-fs-.zip` (~10 MB) | + +## Pitfalls + +There are a few potential incompatibilities in the new compiler: + +- **Inline assembly.** cc65 now optimizes non-volatile inline assembly. + 8bitworkshop runs with `--disable-opt OptLoadStore1` to prevent unexpected changes. +- **`NULL` is now `((void*)0)`** from ``, so headers that + defined their own `NULL` had to stop (see `neslib.h`). +- **Signed constant overflow warning**. For example, `128 << 8` + is now an error-adjacent warning and needs an unsigned constant (`128u << 8`). +- **Recursing into `main()` is an error.** Programs that tail-called + `main()` need an explicit loop instead. +- **`#pragma bss-name`/`data-name` regions must be closed.** Without a + matching `pop`, the newer compiler could place earlier globals into + `ZEROPAGE` and overflow the 256-byte area. (See `nes/chase.c`.) +- **`sp` was renamed to `c_sp`.** The old alias still works but emits an + `ld65` deprecation warning. Assembly code may need to be modified. + +## Warnings vs. errors + +8bitworkshop treats only **errors** as build failures; `Warning:` lines are +ignored so a clean build does not fail on style or version noise. + +## Apple II graphics + +The stock lo-res driver (`a2.lo.s`) required a small upstream +patch (a stray branch in `INSTALL`). \ No newline at end of file diff --git a/src/docs/index.md b/src/docs/index.md index 2a8a6b9e..5ead7511 100644 --- a/src/docs/index.md +++ b/src/docs/index.md @@ -52,6 +52,8 @@ multi-file projects, compiler/linker directives, and file extensions. See [Toolchains & Platforms](toolchains.md) for the full list of tools and the complete platform × extension → tool reference. +CC65 was updated in 8bitworkshop 4.0 -- for more information see +[cc65 Toolchain Upgrade](cc65-upgrade.md). ## Debugger diff --git a/src/ide/views/helpview.ts b/src/ide/views/helpview.ts index 5ad358a1..ce5958c4 100644 --- a/src/ide/views/helpview.ts +++ b/src/ide/views/helpview.ts @@ -22,6 +22,7 @@ import breakpointsMd from "../../docs/breakpoints.md"; import asseteditorMd from "../../docs/asseteditor.md"; import managingFilesMd from "../../docs/managing-files.md"; import buildDirectivesMd from "../../docs/build-directives.md"; +import cc65upgrade from "../../docs/cc65-upgrade.md"; import toolchainsMd from "../../docs/toolchains.md"; import assetHeadersMD from "../../docs/asset-headers.md"; import embeddingIdeMd from "../../docs/embedding-ide.md"; @@ -57,6 +58,7 @@ export const HELP_TOPICS: HelpTopic[] = [ { id: "asseteditor", title: "Asset Editor", html: asseteditorMd }, { id: "managing-files", title: "Managing Files", html: managingFilesMd }, { id: "build-directives", title: "Build Directives", html: buildDirectivesMd }, + { id: "cc65-upgrade", title: "CC65 Upgrade", html: cc65upgrade }, { id: "toolchains", title: "Toolchains & Platforms", html: toolchainsMd }, { id: "asset-headers", title: "Asset Headers", html: assetHeadersMD }, { id: "embedding-ide", title: "Embedding the IDE", html: embeddingIdeMd }, diff --git a/src/platform/apple2.ts b/src/platform/apple2.ts index 36a4fbf6..89922372 100644 --- a/src/platform/apple2.ts +++ b/src/platform/apple2.ts @@ -12,6 +12,7 @@ const APPLE2_PRESETS: Preset[] = [ { id: 'keyboardtest.c', name: 'Keyboard Test' }, { id: 'mandel.c', name: 'Mandelbrot' }, { id: 'tgidemo.c', name: 'TGI Graphics Demo' }, + { id: 'icepiano.c', name: 'Ice Piano (Hires Demo)' }, { id: 'Eliza.c', name: 'Eliza' }, { id: 'siegegame.c', name: 'Siege Game' }, { id: 'cosmic.c', name: 'Cosmic Impalas' }, diff --git a/src/platform/atari8.ts b/src/platform/atari8.ts index bbc8dd53..1e1560e1 100644 --- a/src/platform/atari8.ts +++ b/src/platform/atari8.ts @@ -16,6 +16,7 @@ var Atari8_PRESETS : Preset[] = [ var Atari800_PRESETS = Atari8_PRESETS.concat([ //{id:'testmusic.c', name:'POKEY Music'}, {id:'tgidemo.c', name:'TGI Demo'}, + {id:'icepiano.c', name:'Ice Piano (TGI Demo)'}, {id:'libdemo.c', name:'Library Demo'}, {id:'scrolldemo.c', name:'Scrolling'}, {id:'hello.cpp', name:'Hello Oscar64', category:'Oscar64'}, diff --git a/src/worker/fs/cc65-fs-apple2.zip b/src/worker/fs/cc65-fs-apple2.zip index f305735d..252b05bb 100644 Binary files a/src/worker/fs/cc65-fs-apple2.zip and b/src/worker/fs/cc65-fs-apple2.zip differ diff --git a/src/worker/fs/cc65-fs-atari2600.zip b/src/worker/fs/cc65-fs-atari2600.zip index 551ff9ac..eec4ff9f 100644 Binary files a/src/worker/fs/cc65-fs-atari2600.zip and b/src/worker/fs/cc65-fs-atari2600.zip differ diff --git a/src/worker/fs/cc65-fs-atari8.zip b/src/worker/fs/cc65-fs-atari8.zip index c4498bad..3cbd946a 100644 Binary files a/src/worker/fs/cc65-fs-atari8.zip and b/src/worker/fs/cc65-fs-atari8.zip differ diff --git a/src/worker/fs/cc65-fs-c64.zip b/src/worker/fs/cc65-fs-c64.zip index 4164886d..d197a699 100644 Binary files a/src/worker/fs/cc65-fs-c64.zip and b/src/worker/fs/cc65-fs-c64.zip differ diff --git a/src/worker/fs/cc65-fs-nes.zip b/src/worker/fs/cc65-fs-nes.zip index 8a16a06a..3d7d3258 100644 Binary files a/src/worker/fs/cc65-fs-nes.zip and b/src/worker/fs/cc65-fs-nes.zip differ diff --git a/src/worker/fs/cc65-fs-none.zip b/src/worker/fs/cc65-fs-none.zip index 48a90363..6dd93414 100644 Binary files a/src/worker/fs/cc65-fs-none.zip and b/src/worker/fs/cc65-fs-none.zip differ diff --git a/src/worker/fs/cc65-fs-pce.zip b/src/worker/fs/cc65-fs-pce.zip index c1521dcc..ff125b55 100644 Binary files a/src/worker/fs/cc65-fs-pce.zip and b/src/worker/fs/cc65-fs-pce.zip differ diff --git a/src/worker/fs/cc65-fs-vic20.zip b/src/worker/fs/cc65-fs-vic20.zip index 33bd6d47..f46f11d6 100644 Binary files a/src/worker/fs/cc65-fs-vic20.zip and b/src/worker/fs/cc65-fs-vic20.zip differ diff --git a/src/worker/lib/nes/neslib.h b/src/worker/lib/nes/neslib.h index 526a33b8..76fa2c34 100644 --- a/src/worker/lib/nes/neslib.h +++ b/src/worker/lib/nes/neslib.h @@ -297,7 +297,10 @@ void __fastcall__ nmi_set_callback(void (*callback)(void)); #define NAMETABLE_C 0x2800 #define NAMETABLE_D 0x2c00 -#define NULL 0 +// newer cc65 stddef.h defines NULL as ((void*)0); don't clash with it +#ifndef NULL +#define NULL ((void*)0) +#endif #define TRUE 1 #define FALSE 0 diff --git a/src/worker/tools/cc65.ts b/src/worker/tools/cc65.ts index d9d92e49..6a27e97d 100644 --- a/src/worker/tools/cc65.ts +++ b/src/worker/tools/cc65.ts @@ -12,6 +12,11 @@ import { wasiFSAdapter } from "../wasiutils"; // cc65-fs-.zip, preopened at '.' const CC65_SHARE = 'share/cc65'; +// cc65/ca65/ld65 report both errors and non-fatal warnings as +// ":: : " on stderr; skip the warnings so a +// clean build with only style/deprecation warnings doesn't fail. +const re_cc65_warning = /:\s*Warning:/; + const wasiModules: { [tool: string]: WebAssembly.Module } = {}; /** @@ -187,7 +192,7 @@ export async function assembleCA65(step: BuildStep): Promise { .concat(extraArgsFor('ca65', step.params.buildArgs)); args.splice(args.length - 1, 0, ...extra); const { wasi, errno, stderr } = await runCC65Tool(step, 'ca65', args, (fs) => populateFiles(step, fs)); - stderr.forEach(makeErrorMatcher(errors, /(.+?):(\d+): (.+)/, 2, 3, step.path, 1)); + stderr.filter(s => !re_cc65_warning.test(s)).forEach(makeErrorMatcher(errors, /(.+?):(\d+): (.+)/, 2, 3, step.path, 1)); checkExitCode('ca65', errno, stderr, errors); if (errors.length) { let listings : CodeListingMap = {}; @@ -231,8 +236,13 @@ export async function linkLD65(step: BuildStep): Promise { populateEntry(fs, params.cfgfile, store.getFileEntry(params.cfgfile), null); } }); - // any ld65 message (even a warning) fails the build - for (let s of stderr) errors.push({ msg: s, line: 0 }); + // any non-warning ld65 message fails the build; again, "Warning:"-severity + // output (e.g. the sp->c_sp deprecation from our prebuilt crt0.o/neslib2.lib) + // is non-fatal noise the user can't act on. + for (let s of stderr) { + if (re_cc65_warning.test(s)) continue; + errors.push({ msg: s, line: 0 }); + } checkExitCode('ld65', errno, stderr, errors); if (errors.length) return { errors: errors }; @@ -356,8 +366,13 @@ export async function compileCC65(step: BuildStep): Promise { // //#symbol c / //#flag c args.push.apply(args, defineArgs('cc65', params.symbols && params.symbols.compiler)); args.push.apply(args, extraArgsFor('cc65', params.buildArgs)); - var customArgs = params.extra_compiler_args || ['-T', '-g', '-Oirs', '-Cl', '-W', '-pointer-sign,-no-effect']; - args = args.concat(customArgs, args); + var customArgs = params.extra_compiler_args || ['-T', '-g', '-Oirs', '-Cl', '-W', '-pointer-sign,-no-effect,-unreachable-code']; + // cc65 V2.19-3867 parses inline asm into code entries and a new + // optimizer step (OptLoadStore1) removes a store that follows a load + // from the same address -- wrong for hardware registers and + // self-modifying code. The old cc65 never touched inline asm; keep + // that behavior by disabling just this new step. + args = args.concat(customArgs, ['--disable-opt', 'OptLoadStore1'], args); args.push(step.path); const { wasi, errno, stderr } = await runCC65Tool(step, 'cc65', args, (fs) => { populateFiles(step, fs, { @@ -371,7 +386,7 @@ export async function compileCC65(step: BuildStep): Promise { }); populateExtraFiles(step, fs, params.extra_compile_files); }); - stderr.forEach(makeErrorMatcher(errors, /(.*?):(\d+): (.+)/, 2, 3, step.path, 1)); + stderr.filter(s => !re_cc65_warning.test(s)).forEach(makeErrorMatcher(errors, /(.*?):(\d+): (.+)/, 2, 3, step.path, 1)); checkExitCode('cc65', errno, stderr, errors); if (errors.length) return { errors }; putWorkFile(destpath, readWASIOutputString(wasi, destpath)); diff --git a/src/worker/wasm/ca65.wasm b/src/worker/wasm/ca65.wasm index 8f6d64ec..20b8a2f7 100644 Binary files a/src/worker/wasm/ca65.wasm and b/src/worker/wasm/ca65.wasm differ diff --git a/src/worker/wasm/cc65.wasm b/src/worker/wasm/cc65.wasm index c95156c7..70327e47 100644 Binary files a/src/worker/wasm/cc65.wasm and b/src/worker/wasm/cc65.wasm differ diff --git a/src/worker/wasm/ld65.wasm b/src/worker/wasm/ld65.wasm index 2ed63a86..38b9cb64 100644 Binary files a/src/worker/wasm/ld65.wasm and b/src/worker/wasm/ld65.wasm differ diff --git a/test/cli/testcli.js b/test/cli/testcli.js index 7f520861..72264081 100644 --- a/test/cli/testcli.js +++ b/test/cli/testcli.js @@ -284,6 +284,39 @@ describe('8bws CLI', function () { }); }); + describe('apple2 TGI drivers', function () { + // build a one-file program next to the preset driver, run it, return the script output + function runTGI(body, headers, script) { + var dir = fs.mkdtempSync(path.join(os.tmpdir(), '8bws-tgi-')); + for (var f of ['a2hires.s', 'a2hires.h']) { + fs.copyFileSync(path.join('presets/apple2', f), path.join(dir, f)); + } + var src = path.join(dir, 'main.c'); + fs.writeFileSync(src, headers + '\nint main(void) {\n' + body + '\nfor(;;);\n}\n'); + return cli('run', '--platform', 'apple2', src, '-e', script); + } + it('hires driver should draw pixels and lines on page 2', function () { + var out = runTGI( + 'tgi_install(a2hires_tgi); tgi_init(); tgi_clear();' + + 'tgi_setcolor(3); tgi_setpixel(0, 0); tgi_setpixel(279, 191);' + + 'tgi_setcolor(5); tgi_line(0, 8, 13, 8);', + '#include \n#include "a2hires.h"\n//#link "a2hires.s"', + 'run 20; mem $4000 2; mem $4080 2; mem $5FF0 8'); + assert.ok(/^4000: 01 00/m.test(out), out); // white pixel at 0,0 + assert.ok(/^4080: AA D5/m.test(out), out); // orange line: odd columns + palette bit + assert.ok(/^5FF0: 00 00 00 00 00 00 00 40/m.test(out), out); // last pixel + }); + it('lores driver should install and draw (stray branch in cc65 a2.lo.s)', function () { + var out = runTGI( + 'tgi_install(a2_lo_tgi); tgi_init(); tgi_clear();' + + 'tgi_setcolor(15); tgi_bar(0, 0, 39, 9);', + '#include ', + 'run 30; pc 1; mem $400 4'); + assert.ok(!/PC=\$0000/.test(out), out); // not crashed into zero page + assert.ok(/^0400: (?!00 00 00 00)/m.test(out), out); // lo-res memory was written + }); + }); + describe('legacy aliases', function () { it('should accept compile, check and compilerun', function () { assert.equal(cliJSON('compile', '--platform', 'gb', 'presets/gb/hello.c', '--check').command, 'check'); diff --git a/test/cli/testworker.js b/test/cli/testworker.js index ceef0d7d..a07f80ec 100644 --- a/test/cli/testworker.js +++ b/test/cli/testworker.js @@ -162,6 +162,9 @@ describe('Worker', function() { it('should compile CC65', function(done) { compile('cc65', '#if defined(__8BITWORKSHOP__) && defined(__MAIN__)\nint main() {\nint x=1;\nreturn x+2;\n}\n#endif', 'nes.mame', done, 40976, 3); }); + it('should compile CC65 with a warning (warnings are not errors)', function(done) { + compile('cc65', '#if defined(__8BITWORKSHOP__) && defined(__MAIN__)\nint main() {\nconst const int x=1;\nreturn x+2;\n}\n#endif', 'nes.mame', done, 40976, 3, 0); + }); it('should NOT compile CC65 (compile error)', function(done) { compile('cc65', 'int main() {\nint x=1;\nprintf("%d",x);\nreturn x+2;\n}', 'nes', done, 0, 0, 1); }); @@ -388,7 +391,7 @@ describe('Worker', function() { }); it('should compile apple2 skeleton with CC65', function(done) { var csource = ab2str(fs.readFileSync('presets/apple2/skeleton.cc65')); - compile('cc65', csource, 'apple2', done, 3023+58, 4, 0); + compile('cc65', csource, 'apple2', done, 3382, 4, 0); }); // TODO: test if compile, errors, then compile same file it('should compile CC65 banked', function(done) { @@ -402,7 +405,7 @@ describe('Worker', function() { it('should compile C64 cc65 skeleton', function(done) { var csource = ab2str(fs.readFileSync('presets/c64/skeleton.cc65')); csource = csource.replace('#include "','//'); - compile('cc65', csource, 'c64.wasm', done, 3112, 3, 0); + compile('cc65', csource, 'c64.wasm', done, 3026, 3, 0); }); it('should compile zmachine inform6 skeleton', function(done) { var csource = ab2str(fs.readFileSync('presets/zmachine/skeleton.inform6')); @@ -420,7 +423,7 @@ describe('Worker', function() { doBuild(msgs, done2, 205, 0, 0); }); it('should compile CC65 flags', function(done) { - compile('cc65', '#define CC65_FLAGS -Or,-g,-j\nint main() {\nint x=1;\nreturn x+2;\n}', 'apple2', done, 416+58, 3); + compile('cc65', '#define CC65_FLAGS -Or,-g,-j\nint main() {\nint x=1;\nreturn x+2;\n}', 'apple2', done, 489, 2); }); /* it('should compile gameboy', function(done) { diff --git a/test/unit/testdetect.ts b/test/unit/testdetect.ts index eed67914..91401598 100644 --- a/test/unit/testdetect.ts +++ b/test/unit/testdetect.ts @@ -45,6 +45,24 @@ describe('detect', () => { assert.strictEqual(d[0].platform, 'vcs'); }); + it('tells the Atari 8-bit family apart by header', async () => { + // atari.h is guarded by __ATARI__ (800); atari5200.h by __ATARI5200__ + var d = await detectProject(files({ 'game.c': '#include \nvoid main() {}\n' })); + assert.strictEqual(d[0].platform, 'atari8-800'); + d = await detectProject(files({ 'game.c': '#include \nvoid main() {}\n' })); + assert.strictEqual(d[0].platform, 'atari8-5200'); + }); + + it('prefers the folder name over a shared Atari library header', async () => { + var map: { [fn: string]: string } = { + 'game.c': '#include \nvoid main() {}\n', + 'a8lib.h': '#if defined(__ATARI5200__)\n#include \n#else\n#include \n#endif\n', + }; + var d = await detectProject({ files: Object.keys(map), read: (fn) => map[fn], platforms: PLATFORMS, dirName: 'atari8-800' }); + assert.strictEqual(d[0].platform, 'atari8-800'); + assert.ok(isClearWinner(d)); + }); + it('says nothing about plain C', async () => { var d = await detectProject(files({ 'util.c': 'int add(int a, int b) { return a + b; }\n', 'util.h': 'int add(int, int);\n' })); assert.ok(d.every(x => x.score < 0.5), JSON.stringify(d));