mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-24 08:02:27 +00:00
oscar64 support for atari8, nes
This commit is contained in:
1 parent
bc61aecf59
commit
0c4adef319
14 files changed
+240
-13
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) ;
|
||||
}
|
||||
@@ -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) ;
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
Reference in new issue
Block a user