oscar64 support for atari8, nes

This commit is contained in:
Steven Hugg committed 2026-09-17 14:16:14 +02:00
1 parent bc61aecf59
commit 0c4adef319
14 files changed
+240 -13

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
// Hello World for the Oscar64 optimizing C compiler
// https://github.com/drmortalwombat/oscar64
#include <stdio.h>
#include <conio.h>
int main(void)
{
clrscr();
gotoxy(0, 1);
printf("HELLO, ATARI WORLD!");
return 0;
}
+13
View File
@@ -0,0 +1,13 @@
// Hello World for the Oscar64 optimizing C compiler
// https://github.com/drmortalwombat/oscar64
#include <stdio.h>
#include <conio.h>
int main(void)
{
clrscr();
gotoxy(0, 1);
printf("HELLO, ATARI WORLD!");
return 0;
}
+36
View File
@@ -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 <nes/neslib.h>
// 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) ;
}
+35
View File
@@ -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 <nes/neslib.h>
// 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) ;
}
+7
View File
@@ -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: {
+1 -1
View File
@@ -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 {
+4 -1
View File
@@ -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, {
+1
View File
@@ -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)'},
];
+9 -2
View File
@@ -230,10 +230,11 @@ async function doRun(args: Args, positional: string[]): Promise<void> {
// A source file is built first; a ROM is loaded as-is.
let romFile = input;
let symbols: { [name: string]: number } = {};
let built: Awaited<ReturnType<typeof compileSource>> | 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<void> {
}
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);
+10
View File
@@ -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',
}
}
}
+29 -3
View File
@@ -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<BuildStepResult>
}
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<BuildStepResult>
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<BuildStepResult>
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);
+10 -6
View File
@@ -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,
+63
View File
@@ -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 <stdio.h>\n#include <conio.h>\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 <nes/neslib.h>\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);
+9
View File
@@ -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 () {