diff --git a/presets/atari8-800/hello.cpp b/presets/atari8-800/hello.cpp new file mode 100644 index 00000000..23aaf0f1 --- /dev/null +++ b/presets/atari8-800/hello.cpp @@ -0,0 +1,13 @@ +// Hello World for the Oscar64 optimizing C compiler +// https://github.com/drmortalwombat/oscar64 + +#include +#include + +int main(void) +{ + clrscr(); + gotoxy(0, 1); + printf("HELLO, ATARI WORLD!"); + return 0; +} \ No newline at end of file diff --git a/presets/atari8-800/skeleton.oscar64 b/presets/atari8-800/skeleton.oscar64 new file mode 100644 index 00000000..23aaf0f1 --- /dev/null +++ b/presets/atari8-800/skeleton.oscar64 @@ -0,0 +1,13 @@ +// Hello World for the Oscar64 optimizing C compiler +// https://github.com/drmortalwombat/oscar64 + +#include +#include + +int main(void) +{ + clrscr(); + gotoxy(0, 1); + printf("HELLO, ATARI WORLD!"); + return 0; +} \ No newline at end of file diff --git a/presets/nes/hello.cpp b/presets/nes/hello.cpp new file mode 100644 index 00000000..439346c5 --- /dev/null +++ b/presets/nes/hello.cpp @@ -0,0 +1,36 @@ +/* +A simple "hello world" example using the Oscar64 compiler. + +Place the ASCII font into CHR ROM, set the palette, write a message to +the nametable, and turn on the PPU to display video. +*/ + +#include + +// place the pattern table into CHR ROM (region 0 -> NROM CHR bank) +#pragma section( tiles, 0 ) +#pragma region( tbank, 0x0000, 0x2000, , 0, { tiles } ) +#pragma data(tiles) +__export char tiles[] = { +#embed "jroatch.chr" +}; +#pragma data(data) + +// Oscar64's neslib calls this after console reset +void nes_game(void) { + // set palette colors + pal_col(0, 0x02); // dark blue + pal_col(1, 0x14); // fuchsia + pal_col(2, 0x20); // grey + pal_col(3, 0x30); // white + + // write text to the name table + vram_adr(NTADR_A(2, 2)); + vram_write("HELLO, WORLD!", 13); + + // enable PPU rendering (turn on screen) + ppu_on_all(); + + // infinite loop + while (1) ; +} diff --git a/presets/nes/skeleton.oscar64 b/presets/nes/skeleton.oscar64 new file mode 100644 index 00000000..d447a1e7 --- /dev/null +++ b/presets/nes/skeleton.oscar64 @@ -0,0 +1,35 @@ +/* +NES Oscar64 skeleton. + +Sets up an ASCII font in CHR ROM and the PPU, then draws a message. +Oscar64's neslib calls nes_game() after console reset. +*/ + +#include + +// place the pattern table into CHR ROM (region 0 -> NROM CHR bank) +#pragma section( tiles, 0 ) +#pragma region( tbank, 0x0000, 0x2000, , 0, { tiles } ) +#pragma data(tiles) +__export char tiles[] = { +#embed "jroatch.chr" +}; +#pragma data(data) + +void nes_game(void) { + // set palette colors + pal_col(0, 0x02); // screen color (dark blue) + pal_col(1, 0x20); // grey + pal_col(2, 0x30); // white + pal_col(3, 0x14); // fuchsia + + // draw a message + vram_adr(NTADR_A(2, 2)); + vram_write("HELLO, WORLD!", 13); + + // enable rendering + ppu_on_all(); + + // main loop + while (1) ; +} diff --git a/src/common/toolmeta.ts b/src/common/toolmeta.ts index 52a396b2..9ee85176 100644 --- a/src/common/toolmeta.ts +++ b/src/common/toolmeta.ts @@ -558,10 +558,17 @@ export const TOOL_META: { [id: string]: ToolMeta } = { wasiFSZip: 'oscar64-fs.zip', editorStyle: 'text/x-csrc', helpURL: 'https://github.com/drmortalwombat/oscar64/blob/v1.32.266/oscar64.md', + defineFlag: '-d', defineInline: true, wasmModule: 'oscar64', version: '1.32.266', includePatterns: SHARED_INCLUDE_PATTERNS, linkPatterns: SHARED_LINK_PATTERNS, + platforms: { + // oscar64 defaults to the C64 target machine + c64: {}, + atari8: { buildArgs: { compiler: ['-tm=atari'] } }, + nes: { buildArgs: { compiler: ['-tm=nes'] } }, + }, }, bataribasic: { diff --git a/src/machine/atari8.ts b/src/machine/atari8.ts index c538a72e..5f646df8 100644 --- a/src/machine/atari8.ts +++ b/src/machine/atari8.ts @@ -324,7 +324,7 @@ export class Atari800 extends BasicScanlineMachine implements AcceptsPaddleInput } loadROM(rom: Uint8Array, title: string) { - if ((rom[0] == 0xff && rom[1] == 0xff) && !title?.endsWith('.rom')) { + if ((rom[0] == 0xff && rom[1] == 0xff) && !title.endsWith('.rom')) { // XEX file, chill out and wait for BIOS hook this.xexdata = rom; } else { diff --git a/src/platform/atari8.ts b/src/platform/atari8.ts index 69ae46ee..102310d6 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:'scrolldemo.c', name:'Scrolling'}, + {id:'hello.cpp', name:'Hello Oscar64', category:'C++ (Oscar64)'}, {id:'sieve.bas', name:'Benchmark', category:'FastBasic'}, {id:'pmtest.bas', name:'Sprites Test'}, {id:'dli.bas', name:'DLI Test'}, @@ -99,7 +100,9 @@ abstract class Atari8MAMEPlatform extends BaseMAME6502Platform { } class Atari800MAMEPlatform extends Atari8MAMEPlatform implements Platform { - getPresets() { return Atari800_PRESETS; } + // MAME loads cartridge ROMs; oscar64's atari target emits an XEX image, so it + // only works on the native platform. + getPresets() { return Atari800_PRESETS.filter(p => !p.id.endsWith('.cpp')); } loadROM(title, data) { if (!this.started) { this.startModule(this.mainElement, { diff --git a/src/platform/nes.ts b/src/platform/nes.ts index 678727de..f448150f 100644 --- a/src/platform/nes.ts +++ b/src/platform/nes.ts @@ -46,6 +46,7 @@ const JSNES_PRESETS : Preset[] = [ {id:'xyscroll.dasm', name:'XY Split Scrolling'}, // {id:'scrollrt.dasm', name:'Line-by-line Scrolling'}, {id:'road.dasm', name:'3-D Road Demo'}, + {id:'hello.cpp', name:'Hello Oscar64', category:'Oscar64'}, {id:'chase/game.c', name:'Shiru\'s Chase Game', category:'Other'}, {id:'hello.wiz', name:'Hello (Wiz)'}, ]; diff --git a/src/tools/8bws.ts b/src/tools/8bws.ts index 071629dd..8a95caf1 100644 --- a/src/tools/8bws.ts +++ b/src/tools/8bws.ts @@ -230,10 +230,11 @@ async function doRun(args: Args, positional: string[]): Promise { // A source file is built first; a ROM is loaded as-is. let romFile = input; let symbols: { [name: string]: number } = {}; + let built: Awaited> | undefined; let platformId = str(args, 'platform') || ROM_PLATFORMS[path.extname(input).toLowerCase()]; if (!looksLikeROM(input)) { if (!platformId) fail('run', `Building ${input} requires --platform`); - const built = await compileSource(args, input, platformId); + built = await compileSource(args, input, platformId); romFile = path.join(os.tmpdir(), '8bws-' + path.basename(input).replace(/\.\w+$/, '') + '.rom'); fs.writeFileSync(romFile, Buffer.from(built.rom)); symbols = built.symbolmap; @@ -243,7 +244,13 @@ async function doRun(args: Args, positional: string[]): Promise { } const target = await openTarget(args, platformId); - target.loadROM(new Uint8Array(fs.readFileSync(romFile)), path.basename(romFile)); + // The temp file is always written as .rom, but some platforms use the title + // to pick a load format -- an Atari XEX named .rom would load as a cartridge. + let romTitle = path.basename(romFile); + if (built && target.platform.getROMExtension) { + romTitle = path.basename(romFile, '.rom') + target.platform.getROMExtension(new Uint8Array(built.rom)); + } + target.loadROM(new Uint8Array(fs.readFileSync(romFile)), romTitle); const script = new RunScript(target); script.addSymbols(symbols); diff --git a/src/worker/server/buildenv.ts b/src/worker/server/buildenv.ts index ba399a95..091d21ec 100644 --- a/src/worker/server/buildenv.ts +++ b/src/worker/server/buildenv.ts @@ -99,6 +99,16 @@ const OSCAR64_TOOL: ServerBuildTool = { }, c64: { outfile: 'a.prg', + }, + atari8: { + // the atari target emits a binary-load XEX image + args: ['-Os', '-g', '-d__8BITWORKSHOP__', '-tm=atari', '-o=$OUTFILE', '$INFILES'], + outfile: 'a.xex', + }, + nes: { + // the nes target emits an iNES image with 32K PRG + 8K CHR + args: ['-Os', '-g', '-d__8BITWORKSHOP__', '-tm=nes', '-o=$OUTFILE', '$INFILES'], + outfile: 'a.nes', } } } diff --git a/src/worker/tools/oscar64.ts b/src/worker/tools/oscar64.ts index cfce6012..cd041ab5 100644 --- a/src/worker/tools/oscar64.ts +++ b/src/worker/tools/oscar64.ts @@ -1,4 +1,5 @@ import { WASIFilesystem, WASIRunner } from "../../common/wasi/wasishim"; +import { defineArgs, extraArgsFor } from "../../common/toolmeta"; import { BuildStep, BuildStepResult, gatherFiles, staleFiles, store, putWorkFile } from "../builder"; import { makeErrorMatcher } from "../listingutils"; import { parseOscar64Listing, parseOscar64Lbl, parseOscar64Map } from "./oscar64parse"; @@ -39,7 +40,16 @@ export async function compileOscar64(step: BuildStep): Promise } wasi.addPreopenDirectory("include"); wasi.addPreopenDirectory("."); - wasi.setArgs(["oscar64", "-v", "-g", "-ii=include", "-o=" + destpath, step.path]); + // build with source-level debug info and no optimization, so inlined + // helpers (e.g. neslib's pal_col/vram_adr) keep their call-site line + // info and the editor can show hex offsets for every line. User build + // args are appended after this, so -O1/-O2 still override it. + let args = ["oscar64", "-O1", "-Oz", "-Op", "-Ox", "-g", "-ii=include", "-o=" + destpath]; + args.push.apply(args, defineArgs('oscar64', step.params && step.params.define)); + args.push.apply(args, defineArgs('oscar64', step.params && step.params.symbols && step.params.symbols.compiler)); + args.push.apply(args, extraArgsFor('oscar64', step.params && step.params.buildArgs)); + args.push(step.path); + wasi.setArgs(args); try { wasi.run(); } catch (e) { @@ -59,12 +69,25 @@ export async function compileOscar64(step: BuildStep): Promise if (errors.length) { return { errors }; } - const output = wasi.fs.getFile("./" + destpath).getBytes(); + // oscar64 picks the output extension from the target machine/format + // (e.g. .xex for the atari target), ignoring the one we asked for, and + // writes it as '././name.ext'. Match on the basename + known output + // extensions since the leading './' depth varies. + const prefix = destpath.replace(/\.[^.]+$/, ''); + const basename = prefix.replace(/^.*\//, ''); + let outpath = destpath; + for (const ext of [".xex", ".prg", ".crt", ".bin", ".nes"]) { + if (wasi.fs.getFile(prefix + ext)) { outpath = prefix + ext; break; } + let found = wasi.fs.getFiles().find((f) => f.name.endsWith("/" + basename + ext)); + if (found) { outpath = found.name; break; } + } + const output = wasi.fs.getFile(outpath).getBytes(); putWorkFile(destpath, output); // read and parse oscar64 auxiliary output files (.map, .lbl, .asm) - const prefix = destpath.replace(/\.prg$/, ''); let mapout = getWasiFileAsString(wasi, prefix + ".map") || getWasiFileAsString(wasi, ".map"); let lblout = getWasiFileAsString(wasi, prefix + ".lbl") || getWasiFileAsString(wasi, ".lbl"); + // banked targets (NES) emit a Mesen label file (.mlb) instead of a VICE .lbl + let mlbout = getWasiFileAsString(wasi, prefix + ".mlb") || getWasiFileAsString(wasi, ".mlb"); let asmout = getWasiFileAsString(wasi, prefix + ".asm") || getWasiFileAsString(wasi, ".asm"); let segments = []; let symbolmap = {}; @@ -79,6 +102,9 @@ export async function compileOscar64(step: BuildStep): Promise symbolmap = Object.assign(parseOscar64Lbl(lblout), symbolmap); putWorkFile(prefix + ".lbl", lblout); } + if (mlbout) { + putWorkFile(prefix + ".mlb", mlbout); + } let listings = {}; if (asmout) { let { srclines, asmlines } = parseOscar64Listing(asmout, step.path); diff --git a/src/worker/tools/oscar64parse.ts b/src/worker/tools/oscar64parse.ts index fdff6203..5eab95e0 100644 --- a/src/worker/tools/oscar64parse.ts +++ b/src/worker/tools/oscar64parse.ts @@ -37,7 +37,8 @@ export function parseOscar64Map(mapout: string) { continue; } // "0880 - 0889 : DATA, code" or "0801 - 0853 : startup, NATIVE_CODE:startup" - let m = /^([0-9a-f]+) - ([0-9a-f]+) : ([^,]+), (.+)$/.exec(line); + // banked targets (NES) prefix the start address with the bank: "00:8000 - 8085 : main, NATIVE_CODE:code" + let m = /^(?:[0-9a-f]{2}:)?([0-9a-f]+) - (?:[0-9a-f]{2}:)?([0-9a-f]+) : ([^,]+), (.+)$/.exec(line); if (m) { const start = parseInt(m[1], 16); const end = parseInt(m[2], 16); @@ -101,8 +102,9 @@ export function parseOscar64Listing(asmout: string, asmfn: string) { let asm_lineno = 0; // ; 2, "/test.c" let re_src = /^;\s*(\d+), "(.+?)"/; - // 0801 : 0b __ __ INV - let re_insn = /^([0-9a-f]+) : ([0-9a-f _]{8}) (.*)/; + // "0801 : 0b __ __ INV" (single bank) + // or banked (NES): "00:8000 : a9 00 __ LDA #$00" + let re_insn = /^(?:([0-9a-f]{2}):)?([0-9a-f]{4}) : ([0-9a-f_]{2} [0-9a-f_]{2} [0-9a-f_]{2}) (.*)/; for (let line of asmout.split('\n')) { asm_lineno++; let m2 = re_src.exec(line); @@ -113,9 +115,11 @@ export function parseOscar64Listing(asmout: string, asmfn: string) { } let m = re_insn.exec(line); if (m) { - let offset = parseInt(m[1], 16); - let hex = m[2]; - let asm = m[3]; + let asm = m[4]; + // BSS declarations have no bytes; they are not instructions + if (/^BSS\b/.test(asm)) continue; + let offset = parseInt(m[2], 16); + let hex = m[3]; let insns = (hex + ' ' + asm).trim(); asmlines.push({ line: asm_lineno, diff --git a/test/cli/testworker.js b/test/cli/testworker.js index 856738fb..ee413f2b 100644 --- a/test/cli/testworker.js +++ b/test/cli/testworker.js @@ -194,6 +194,69 @@ describe('Worker', function() { assert.ok(result.segments && result.segments.length > 0, 'no segments'); }); + it('should compile oscar64 for atari8 into an XEX image', async function() { + var msgs = [{code:'#include \n#include \nint main() { clrscr(); printf("FOO"); return 0; }', platform:'atari8-800', tool:'oscar64', path:'main.c', mainfile:true}]; + var result = await new Promise(function(resolve, reject) { + global.postMessage = function(msg) { + if (!msg.unchanged) { + assert.ok(!msg.errors || msg.errors.length === 0, JSON.stringify(msg.errors)); + resolve(msg); + } + }; + (async function() { + await global.onmessage({data:{reset:true}}); + await global.onmessage({data:msgs[0]}); + })().catch(reject); + }); + // the atari target writes a binary-load file: 0xFFFF header, then a segment at 0x2000 + var out = result.output; + assert.ok(out.length > 6, 'output too small: ' + out.length); + assert.equal(out[0], 0xff, 'missing XEX header'); + assert.equal(out[1], 0xff, 'missing XEX header'); + assert.equal(out[2], 0x00, 'unexpected XEX load address lo'); + assert.equal(out[3], 0x20, 'unexpected XEX load address hi'); + }); + + it('should compile oscar64 for nes into an iNES image', async function() { + var src = '#include \n' + + '#pragma section( tiles, 0 )\n' + + '#pragma region( tbank, 0x0000, 0x2000, , 0, { tiles } )\n' + + '#pragma data(tiles)\n' + + '__export char tiles[16] = { 0 };\n' + + '#pragma data(data)\n' + + 'void nes_game(void) { ppu_on_all(); while (1) ; }\n'; + var msgs = [{code:src, platform:'nes', tool:'oscar64', path:'main.c', mainfile:true}]; + var result = await new Promise(function(resolve, reject) { + global.postMessage = function(msg) { + if (!msg.unchanged) { + assert.ok(!msg.errors || msg.errors.length === 0, JSON.stringify(msg.errors)); + resolve(msg); + } + }; + (async function() { + await global.onmessage({data:{reset:true}}); + await global.onmessage({data:msgs[0]}); + })().catch(reject); + }); + // iNES header: 'N', 'E', 'S', 0x1a, then 2 PRG banks and 1 CHR bank + var out = result.output; + assert.ok(out.length === 16 + 0x8000 + 0x2000, 'unexpected size: ' + out.length); + assert.equal(out[0], 0x4e, 'missing iNES header'); + assert.equal(out[1], 0x45, 'missing iNES header'); + assert.equal(out[2], 0x53, 'missing iNES header'); + assert.equal(out[3], 0x1a, 'missing iNES header'); + assert.equal(out[4], 0x02, 'unexpected PRG bank count'); + assert.equal(out[5], 0x01, 'unexpected CHR bank count'); + // NES listings are banked ("00:8000 : ..."); the parser must still map + // C source lines to their 16-bit offsets so the editor shows hex addresses + var keys = Object.keys(result.listings || {}); + assert.ok(keys.length > 0, 'no listings'); + var lst = result.listings[keys[0]]; + assert.ok(lst.lines.length > 0, 'no source lines'); + assert.ok(lst.lines[0].offset >= 0x8000, 'source line missing NES offset: ' + lst.lines[0].offset); + assert.ok(result.symbolmap && result.symbolmap['nes_game'] >= 0x8000, 'no nes_game symbol'); + }); + it('should compile mw8080 skeleton', function(done) { var csource = ab2str(fs.readFileSync('presets/mw8080bw/skeleton.sdcc')); compile('sdcc', csource, 'mw8080bw', done, 8192, 84, 0); diff --git a/test/unit/testbuildparams.ts b/test/unit/testbuildparams.ts index cbb6c286..98e70468 100644 --- a/test/unit/testbuildparams.ts +++ b/test/unit/testbuildparams.ts @@ -293,6 +293,15 @@ describe("platform tool config resolution", function () { assert.strictEqual(getPlatformToolConfig('dasm', 'nes'), undefined); }); + it("routes oscar64 to the target machine for each platform", function () { + // oscar64 defaults to the C64; atari8 needs -tm=atari (and emits a XEX) + assert.deepStrictEqual(extraArgsFor('oscar64', getPlatformToolConfig('oscar64', 'atari8-800').buildArgs), ['-tm=atari']); + assert.deepStrictEqual(extraArgsFor('oscar64', getPlatformToolConfig('oscar64', 'atari8').buildArgs), ['-tm=atari']); + assert.deepStrictEqual(extraArgsFor('oscar64', getPlatformToolConfig('oscar64', 'nes').buildArgs), ['-tm=nes']); + assert.deepStrictEqual(extraArgsFor('oscar64', getPlatformToolConfig('oscar64', 'c64').buildArgs), []); + assert.deepStrictEqual(defineArgs('oscar64', ['FOO=1']), ['-dFOO=1']); + }); + }); describe("tool define/symbol formatting", function () {