Checkpoint
This commit is contained in:
parent
81694c5971
commit
cbae131d0c
5 changed files with 671 additions and 41 deletions
83
STATUS.md
83
STATUS.md
|
|
@ -81,11 +81,16 @@ which runs correctly under MAME (apple2gs).
|
|||
abort(), SIGINT/SIGTERM call exit(128+sig), others ignored.
|
||||
- `<locale.h>`: setlocale always returns "C"; localeconv returns
|
||||
a fixed C-locale lconv struct.
|
||||
- C++ subset: classes, single inheritance, virtual functions,
|
||||
polymorphism via base-class pointer arrays, virtual dtors.
|
||||
Compile with `clang++ -fno-exceptions -fno-rtti`. Multiple
|
||||
inheritance with virtual bases, full RTTI, exceptions are
|
||||
out of scope.
|
||||
- C++ subset: classes, single inheritance, multiple inheritance
|
||||
(Drawable+Movable through one Sprite), virtual base diamond
|
||||
(A and B virtually derive Base; Diamond inherits from both
|
||||
with one shared Base subobject), virtual functions,
|
||||
polymorphism via base-class pointer arrays, virtual dtors,
|
||||
this-pointer adjustment for non-leftmost bases, vbase offset
|
||||
tables. Compile with `clang++ -fno-exceptions -fno-rtti`.
|
||||
Full RTTI (`dynamic_cast`, `typeid`) and exceptions remain
|
||||
out of scope — those need libcxxabi (`__dynamic_cast`,
|
||||
`__cxa_throw`, unwind tables, personality routine).
|
||||
|
||||
**Toolchain:**
|
||||
|
||||
|
|
@ -105,21 +110,24 @@ which runs correctly under MAME (apple2gs).
|
|||
image addresses.
|
||||
- `runtime/build.sh` builds crt0, libc, soft-float, soft-double,
|
||||
libgcc into linkable objects.
|
||||
- `scripts/smokeTest.sh` runs 113 end-to-end checks at -O2:
|
||||
- `scripts/smokeTest.sh` runs 120 end-to-end checks at -O2:
|
||||
scalar ops, control flow, calling conventions, MAME execution
|
||||
regressions, link816 bss-base safety + weak-symbol resolution +
|
||||
heap_end-vs-heap_start sanity, iigs/toolbox.h compile + link,
|
||||
standalone runtime headers, AsmPrinter peepholes (STZ / PEA /
|
||||
PEI — single-STA, shared-LDA-multi-STA, DPF0-forwarding),
|
||||
malloc/free coalesce ordering, plus real-world coverage:
|
||||
Conway's Game of Life blinker (2D loop + neighbour bounds),
|
||||
binary search tree (recursive struct + malloc), function-pointer
|
||||
dispatch table (indirect JSL via `__jsl_indir`), memory-backed
|
||||
file I/O (mfsRegister + fopen/fread/fwrite/fseek/fprintf), C++
|
||||
polymorphism (single inheritance + virtual functions), wchar /
|
||||
signal core APIs, hex dumper writing through fprintf, JSON
|
||||
tokenizer state machine, scripts/bench.sh size-vs-Calypsi
|
||||
harness. 100% pass.
|
||||
iigs/gsos.h compile + link, standalone runtime headers,
|
||||
AsmPrinter peepholes (STZ / PEA / PEI — single-STA, shared-
|
||||
LDA-multi-STA, DPF0-forwarding), malloc/free coalesce ordering,
|
||||
plus real-world coverage: Conway's Game of Life blinker
|
||||
(2D loop + neighbour bounds), binary search tree (recursive
|
||||
struct + malloc), function-pointer dispatch table (indirect
|
||||
JSL via `__jsl_indir`), memory-backed file I/O (mfsRegister +
|
||||
fopen/fread/fwrite/fseek/fprintf), C++ polymorphism (single
|
||||
inheritance), C++ multiple inheritance (Drawable+Movable),
|
||||
C++ virtual base diamond, wchar / signal core APIs, hex dumper
|
||||
writing through fprintf, JSON tokenizer state machine,
|
||||
hash-table command shell (parser + dispatch + chained
|
||||
collisions over fprintf-to-mfs), scripts/bench.sh size-vs-
|
||||
Calypsi harness. 100% pass.
|
||||
|
||||
- `scripts/bench.sh` compiles a microbenchmark suite with both
|
||||
clang (this toolchain) and Calypsi cc65816, comparing emitted
|
||||
|
|
@ -197,27 +205,24 @@ RAM through $FFFF, gaining 8KB of bank-0 space.)
|
|||
|
||||
## Yet to come
|
||||
|
||||
- **GS/OS-backed `<stdio.h>` file I/O** — current FS is
|
||||
memory-backed (programs `mfsRegister` buffers as files). A
|
||||
GS/OS backend would let programs see the real ProDOS volume
|
||||
during MAME execution, but needs Tool Locator init in crt0
|
||||
and a class-1 parm-block dispatch wrapper around $E100A8.
|
||||
- **C++ full RTTI + exceptions** — multi-inheritance and virtual
|
||||
base diamonds work; `dynamic_cast` and `throw`/`try`/`catch`
|
||||
do not. Both need libcxxabi (`__dynamic_cast` walks the
|
||||
type_info hierarchy; `__cxa_throw`/_Unwind_*/personality
|
||||
routine drive stack unwinding). Reasonable to defer until
|
||||
someone wants exception-based code on the IIgs.
|
||||
|
||||
- **C++ exceptions / RTTI / multiple inheritance with virtual
|
||||
bases** — only the `-fno-exceptions -fno-rtti` subset is
|
||||
supported. `__cxa_throw` etc. would need an unwind ABI on
|
||||
this target plus a personality routine.
|
||||
- **Close the size gap to Calypsi further** — `scripts/bench.sh`
|
||||
shows clang at ~2.2x Calypsi text size on the microbenchmarks,
|
||||
sumOfSquares worst at 6.45x (__mulsi3 dispatch). Calypsi's
|
||||
edge is structural: it uses `(sr,s),Y` for stack-relative
|
||||
indirection where we route through DP $E0 indirect-long for
|
||||
bank safety. Targeted opportunities: inline 16x16→32
|
||||
multiply for small operands; widen IMG-slot heuristic so
|
||||
greedy reaches further before spilling.
|
||||
|
||||
- **Close the size gap to Calypsi** — `scripts/bench.sh`
|
||||
shows clang at ~2.2x Calypsi text size on the included
|
||||
microbenchmarks, with sumOfSquares as the worst case (6.45x)
|
||||
due to __mulsi3 dispatch overhead. Targeted improvements:
|
||||
inline 16x16->32 multiply for small operands; widen the
|
||||
IMG slot heuristic so greedy uses them more aggressively;
|
||||
cycle-time benchmark harness (separate from size).
|
||||
|
||||
- **Larger/real-world end-to-end programs** — current real-world
|
||||
smoke (Game of Life, BST, dispatch, hex dumper, JSON tokenizer)
|
||||
exercises core idioms. A multi-thousand-line program (e.g.
|
||||
a small interactive shell, a text editor command loop) would
|
||||
catch issues no smaller test reaches.
|
||||
- **GS/OS file I/O exercised under MAME** — wrappers
|
||||
(`runtime/include/iigs/gsos.h` + `runtime/src/iigsGsos.s`)
|
||||
compile and link, but the smoke harness can't drive them
|
||||
(no ProDOS volume mounted). Validating end-to-end needs a
|
||||
2img/po/dsk launched as a MAME hard disk plus toolbox init.
|
||||
|
|
|
|||
94
runtime/include/iigs/gsos.h
Normal file
94
runtime/include/iigs/gsos.h
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// IIgs GS/OS file I/O wrappers.
|
||||
//
|
||||
// GS/OS calls dispatch through $E100A8 with X holding the call number
|
||||
// and a 16-bit pointer to a class-1 parameter block pushed on the
|
||||
// stack. The parm block layout is per-call but always begins with a
|
||||
// pCount field giving the number of parameters used.
|
||||
//
|
||||
// These wrappers are STUBS — they construct the parm blocks and call
|
||||
// the dispatcher, but require the caller to have a real ProDOS volume
|
||||
// mounted (or equivalent GS/OS volume) for the calls to succeed.
|
||||
// Without that, the dispatcher returns an error code or hangs.
|
||||
//
|
||||
// To use these in MAME smoke tests you'd need:
|
||||
// - A 2img / po / dsk image containing a ProDOS volume
|
||||
// - MAME launched with the image as floppy or hard-disk
|
||||
// - Tool Locator + GS/OS init via iigsToolboxInit()
|
||||
// None of which the current smoke harness provides — these wrappers
|
||||
// are infrastructure for a future GS/OS-aware test rig.
|
||||
//
|
||||
// Class-1 GS/OS calls (pCount-prefixed):
|
||||
// $2010 Open
|
||||
// $2012 Read
|
||||
// $2013 Write
|
||||
// $2014 Close
|
||||
// $2026 GetEOF
|
||||
// $2027 SetEOF
|
||||
// $2029 Quit (special — no return)
|
||||
// See "GS/OS Reference" for the full ~50 calls and parm-block layouts.
|
||||
|
||||
#ifndef IIGS_GSOS_H
|
||||
#define IIGS_GSOS_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// GS/OS string descriptor: 2-byte length + char data (no NUL).
|
||||
// Use with caller-allocated storage:
|
||||
// struct { unsigned short len; char text[14]; } pname = { 6, "MYFILE" };
|
||||
typedef struct {
|
||||
unsigned short length;
|
||||
char text[1]; // variable-length; total size = length + 2
|
||||
} GSString;
|
||||
|
||||
// Class-1 Open parm block.
|
||||
typedef struct {
|
||||
unsigned short pCount; // 2 (or up to 12 for full options)
|
||||
unsigned short refNum; // [out] file reference number
|
||||
void *pathname; // [in] GSString *
|
||||
// Optional fields (if pCount > 2): requestAccess, resourceNumber,
|
||||
// accessRequested, optionList, ... — left out for the basic open.
|
||||
} OpenParm;
|
||||
|
||||
// Class-1 Read/Write parm block.
|
||||
typedef struct {
|
||||
unsigned short pCount; // 4
|
||||
unsigned short refNum; // [in] file reference
|
||||
void *dataBuffer; // [in] in-bank pointer to buffer
|
||||
unsigned long requestCount; // [in] bytes requested
|
||||
unsigned long transferCount;// [out] bytes actually transferred
|
||||
} IORecGS;
|
||||
|
||||
// Class-1 Close / GetEOF / SetEOF / etc. — simple refNum-only blocks.
|
||||
typedef struct {
|
||||
unsigned short pCount; // 1
|
||||
unsigned short refNum;
|
||||
} RefNumRecGS;
|
||||
|
||||
typedef struct {
|
||||
unsigned short pCount; // 2
|
||||
unsigned short refNum;
|
||||
unsigned long eof;
|
||||
} EOFRecGS;
|
||||
|
||||
// Open / Read / Write / Close wrappers. Each returns 0 on success or
|
||||
// a non-zero GS/OS error code (see gsos.h reference for codes). The
|
||||
// parm block lives on the caller's stack; you set the input fields
|
||||
// before the call and read output fields after.
|
||||
//
|
||||
// Implementation lives in runtime/src/iigsGsos.s — needed because the
|
||||
// W65816 backend's inline asm can't take memory operands for the
|
||||
// parm-block address.
|
||||
extern unsigned short gsosOpen (OpenParm *p);
|
||||
extern unsigned short gsosRead (IORecGS *p);
|
||||
extern unsigned short gsosWrite (IORecGS *p);
|
||||
extern unsigned short gsosClose (RefNumRecGS *p);
|
||||
extern unsigned short gsosGetEOF (EOFRecGS *p);
|
||||
extern unsigned short gsosSetEOF (EOFRecGS *p);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // IIGS_GSOS_H
|
||||
70
runtime/src/iigsGsos.s
Normal file
70
runtime/src/iigsGsos.s
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
; iigsGsos.s — GS/OS class-1 dispatch wrappers.
|
||||
;
|
||||
; Each wrapper takes a 16-bit pointer to a class-1 parm block in A
|
||||
; (the C ABI). The dispatcher convention is:
|
||||
; PEA <parm-block-addr-low> ; push 16-bit ptr (low half of 32-bit
|
||||
; PEA 0 ; long pointer; high half is 0)
|
||||
; LDX #<call-number>
|
||||
; JSL $E100A8
|
||||
; Returns the call status in A (0 = success, non-zero = error code).
|
||||
;
|
||||
; All wrappers preserve nothing — the GS/OS dispatcher clobbers A,
|
||||
; X, Y, P. Each takes the parm-block pointer in A (i16) and pushes
|
||||
; it as a 32-bit pointer (low half = the in-bank ptr, high half = 0
|
||||
; for bank-0 parm blocks, which is what we always use).
|
||||
|
||||
.text
|
||||
.globl gsosOpen
|
||||
.globl gsosRead
|
||||
.globl gsosWrite
|
||||
.globl gsosClose
|
||||
.globl gsosGetEOF
|
||||
.globl gsosSetEOF
|
||||
|
||||
; Common dispatch helper macro: arg in A, call number in X.
|
||||
; Pushes the 32-bit parm-block pointer, JSLs the dispatcher, returns
|
||||
; status in A. All wrappers below follow the same shape — copy/paste
|
||||
; rather than macro because the assembler doesn't have a portable
|
||||
; macro syntax we rely on.
|
||||
|
||||
gsosOpen:
|
||||
pha ; push parm-block low
|
||||
pea 0 ; push parm-block high (0 for bank 0)
|
||||
ldx #0x2010
|
||||
jsl 0xe100a8
|
||||
rtl
|
||||
|
||||
gsosRead:
|
||||
pha
|
||||
pea 0
|
||||
ldx #0x2012
|
||||
jsl 0xe100a8
|
||||
rtl
|
||||
|
||||
gsosWrite:
|
||||
pha
|
||||
pea 0
|
||||
ldx #0x2013
|
||||
jsl 0xe100a8
|
||||
rtl
|
||||
|
||||
gsosClose:
|
||||
pha
|
||||
pea 0
|
||||
ldx #0x2014
|
||||
jsl 0xe100a8
|
||||
rtl
|
||||
|
||||
gsosGetEOF:
|
||||
pha
|
||||
pea 0
|
||||
ldx #0x2019
|
||||
jsl 0xe100a8
|
||||
rtl
|
||||
|
||||
gsosSetEOF:
|
||||
pha
|
||||
pea 0
|
||||
ldx #0x2018
|
||||
jsl 0xe100a8
|
||||
rtl
|
||||
152
scripts/benchCycles.sh
Executable file
152
scripts/benchCycles.sh
Executable file
|
|
@ -0,0 +1,152 @@
|
|||
#!/usr/bin/env bash
|
||||
# benchCycles.sh — measure benchmark cycle counts in MAME.
|
||||
#
|
||||
# For each benchmark in benchmarks/, build a wrapper that calls the
|
||||
# benchmark function in a loop with fixed input, records the IIgs CPU
|
||||
# cycle counter before/after via MAME's Lua interface, and writes the
|
||||
# delta to a known memory address. Output is a markdown table: per
|
||||
# benchmark, the cycles per call.
|
||||
#
|
||||
# This is a separate harness from bench.sh (which measures only code
|
||||
# size). Cycle measurement requires a full MAME run per benchmark
|
||||
# (~5 seconds each) so don't run on every smoke pass.
|
||||
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
BENCH_DIR="$PROJECT_ROOT/benchmarks"
|
||||
|
||||
CLANG="$PROJECT_ROOT/tools/llvm-mos-build/bin/clang"
|
||||
LLVM_MC="$PROJECT_ROOT/tools/llvm-mos-build/bin/llvm-mc"
|
||||
LINK="$PROJECT_ROOT/tools/link816"
|
||||
|
||||
oCrt0=$(mktemp --suffix=.o)
|
||||
oLibgcc=$(mktemp --suffix=.o)
|
||||
"$LLVM_MC" -arch=w65816 -filetype=obj "$PROJECT_ROOT/runtime/src/crt0.s" -o "$oCrt0"
|
||||
"$LLVM_MC" -arch=w65816 -filetype=obj "$PROJECT_ROOT/runtime/src/libgcc.s" -o "$oLibgcc"
|
||||
|
||||
# Per-benchmark wrapper template. The C wrapper calls each benchmark
|
||||
# with appropriate inputs, then writes the iteration count and cycle
|
||||
# delta to bank 2. We use clock() (VBL counter, 60 Hz) as a coarse
|
||||
# timer — enough to compare relative speeds.
|
||||
benchInputs() {
|
||||
case "$1" in
|
||||
sumOfSquares) echo 'sumOfSquares(50)';;
|
||||
fib) echo 'fib(10)';;
|
||||
strcpy) echo 'mystrcpy(dst, "hello world!")';;
|
||||
memcmp) echo 'mymemcmp("hello", "hello", 5)';;
|
||||
bsearch) echo 'bsearch(arr, 8, 5)';;
|
||||
dotProduct) echo 'dotProduct(va, vb, 4)';;
|
||||
popcount) echo 'popcount(0x12345678UL)';;
|
||||
crc32) echo 'crc32((const unsigned char *)"hello", 5)';;
|
||||
*) echo "/* unknown */";;
|
||||
esac
|
||||
}
|
||||
|
||||
benchExtern() {
|
||||
case "$1" in
|
||||
sumOfSquares) echo 'extern unsigned long sumOfSquares(unsigned short n);';;
|
||||
fib) echo 'extern unsigned short fib(unsigned short n);';;
|
||||
strcpy) echo 'extern char *mystrcpy(char *d, const char *s); static char dst[16];';;
|
||||
memcmp) echo 'extern int mymemcmp(const void *a, const void *b, unsigned int n);';;
|
||||
bsearch) echo 'extern int bsearch(const int *arr, int n, int key); static const int arr[] = {1,2,3,4,5,6,7,8};';;
|
||||
dotProduct) echo 'extern long dotProduct(const short *a, const short *b, unsigned int n); static const short va[] = {1,2,3,4}; static const short vb[] = {5,6,7,8};';;
|
||||
popcount) echo 'extern int popcount(unsigned long x);';;
|
||||
crc32) echo 'extern unsigned long crc32(const unsigned char *p, unsigned int n);';;
|
||||
*) echo '';;
|
||||
esac
|
||||
}
|
||||
|
||||
# Run one benchmark in MAME with cycle measurement.
|
||||
runOneBench() {
|
||||
local name="$1"
|
||||
local extern_decl
|
||||
local call_expr
|
||||
extern_decl=$(benchExtern "$name")
|
||||
call_expr=$(benchInputs "$name")
|
||||
if [ -z "$extern_decl" ] || [ "$call_expr" = "/* unknown */" ]; then
|
||||
echo "(no input config)"
|
||||
return
|
||||
fi
|
||||
|
||||
local cwrap=$(mktemp --suffix=.c)
|
||||
local owrap=$(mktemp --suffix=.o)
|
||||
local obench=$(mktemp --suffix=.o)
|
||||
local bin=$(mktemp --suffix=.bin)
|
||||
|
||||
cat > "$cwrap" <<EOF
|
||||
$extern_decl
|
||||
__attribute__((noinline)) static void switchToBank2(void) {
|
||||
__asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n");
|
||||
}
|
||||
// Read VBL bit + scan-line position from the IIgs Mega II registers.
|
||||
// \$C02E (VertCnt low) increments at HBL rate (~15.7 kHz), wrapping at
|
||||
// 256. Higher resolution than the soft-VBL counter at \$E1006B; works
|
||||
// without ROM IRQ handling.
|
||||
__attribute__((noinline)) static unsigned char readVbl(void) {
|
||||
unsigned char r;
|
||||
__asm__ volatile ("sep #0x20\nlda 0xc02e\nrep #0x20\nand #0x00ff\n"
|
||||
: "=a"(r) : : "memory");
|
||||
return r;
|
||||
}
|
||||
volatile unsigned long sink;
|
||||
#define ITERS 100
|
||||
int main(void) {
|
||||
// Re-enable IRQs so the IIgs ROM's VBL handler runs and the
|
||||
// VBL counter at \$E1006B actually ticks. crt0 disables IRQs
|
||||
// for safety; the cycle bench needs them on for the timer.
|
||||
__asm__ volatile ("cli\n" ::: "memory");
|
||||
unsigned char t0 = readVbl();
|
||||
for (int i = 0; i < ITERS; i++) {
|
||||
sink = (unsigned long)($call_expr);
|
||||
}
|
||||
unsigned char t1 = readVbl();
|
||||
__asm__ volatile ("sei\n" ::: "memory");
|
||||
unsigned char dt = t1 - t0; // VBL ticks; wraps at 256
|
||||
switchToBank2();
|
||||
*(volatile unsigned short *)0x5000 = (unsigned short)dt;
|
||||
*(volatile unsigned short *)0x5002 = (unsigned short)(sink & 0xFFFF);
|
||||
while (1) {}
|
||||
}
|
||||
EOF
|
||||
|
||||
"$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cwrap" -o "$owrap" 2>/dev/null \
|
||||
|| { echo "compile-fail"; rm -f "$cwrap" "$owrap"; return; }
|
||||
"$CLANG" --target=w65816 -O2 -ffunction-sections -c "$BENCH_DIR/$name.c" -o "$obench" 2>/dev/null \
|
||||
|| { echo "compile-fail"; rm -f "$cwrap" "$owrap" "$obench"; return; }
|
||||
"$LINK" -o "$bin" --text-base 0x1000 "$oCrt0" "$oLibgcc" "$owrap" "$obench" 2>/dev/null \
|
||||
|| { echo "link-fail"; rm -f "$cwrap" "$owrap" "$obench" "$bin"; return; }
|
||||
|
||||
# Read VBL delta at $025000.
|
||||
local val
|
||||
val=$(bash "$PROJECT_ROOT/scripts/runInMame.sh" "$bin" 0x025000 0000 2>&1 \
|
||||
| grep -oE 'val=0x[0-9a-f]+' | head -1 | sed 's/val=0x//')
|
||||
rm -f "$cwrap" "$owrap" "$obench" "$bin"
|
||||
|
||||
if [ -z "$val" ]; then
|
||||
echo "(no read)"
|
||||
else
|
||||
# \$C02E ticks at HBL rate. IIgs has ~65 cycles per HBL at
|
||||
# native 2.6 MHz, so each tick ≈ 65 cycles. We ran 100
|
||||
# iterations, so per-iter cycles ≈ ticks * 65 / 100. For
|
||||
# very fast benches, 100 iters may not cross a tick — bump
|
||||
# the constant in the C wrapper if you need finer resolution.
|
||||
local ticks=$((16#$val))
|
||||
if [ "$ticks" -eq 0 ]; then
|
||||
echo "<65 cyc/iter (under timer resolution)"
|
||||
else
|
||||
local cycles=$((ticks * 65 / 100))
|
||||
printf "%d hbl-ticks (~%d cyc/iter)" "$ticks" "$cycles"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
printf '| Benchmark | Per-iteration cycles |\n'
|
||||
printf '|-----------|---------------------:|\n'
|
||||
for src in "$BENCH_DIR"/*.c; do
|
||||
name=$(basename "$src" .c)
|
||||
result=$(runOneBench "$name")
|
||||
printf '| %s | %s |\n' "$name" "$result"
|
||||
done
|
||||
|
||||
rm -f "$oCrt0" "$oLibgcc"
|
||||
|
|
@ -3752,8 +3752,9 @@ EOF
|
|||
# C++ subset: classes, single inheritance, virtual functions,
|
||||
# polymorphism via base-class pointer arrays, virtual dtors.
|
||||
# Compiled with -fno-exceptions -fno-rtti (the supported subset
|
||||
# — full RTTI / exceptions / multi-inheritance with virtual
|
||||
# bases are not supported).
|
||||
# — full RTTI dynamic_cast and exception machinery require
|
||||
# libcxxabi which we don't ship; multi-inheritance and virtual
|
||||
# base diamonds DO work and are exercised below).
|
||||
log "check: MAME runs C++ polymorphism (virtuals + single inheritance)"
|
||||
cppFile="$(mktemp --suffix=.cpp)"
|
||||
oCppFile="$(mktemp --suffix=.o)"
|
||||
|
|
@ -3818,6 +3819,108 @@ EOF
|
|||
fi
|
||||
rm -f "$cppFile" "$oCppFile" "$binCppFile"
|
||||
|
||||
# C++ multiple inheritance (no virtual bases): two abstract
|
||||
# interfaces, one concrete class implementing both. Calls via
|
||||
# both base pointers must dispatch correctly through their
|
||||
# respective vtable slices (the second base lives at non-zero
|
||||
# offset from the object's start; the cast adjusts the ptr).
|
||||
log "check: MAME runs C++ multiple inheritance (Drawable+Movable)"
|
||||
cppMiFile="$(mktemp --suffix=.cpp)"
|
||||
oCppMiFile="$(mktemp --suffix=.o)"
|
||||
binCppMiFile="$(mktemp --suffix=.bin)"
|
||||
cat > "$cppMiFile" <<'EOF'
|
||||
extern "C" __attribute__((noinline)) void switchToBank2(void) {
|
||||
__asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n");
|
||||
}
|
||||
class Drawable { public: virtual int draw() const = 0; virtual ~Drawable() {} };
|
||||
class Movable { public: virtual int move(int dx) const = 0; virtual ~Movable() {} };
|
||||
class Sprite : public Drawable, public Movable {
|
||||
int x;
|
||||
public:
|
||||
Sprite(int x_) : x(x_) {}
|
||||
int draw() const override { return x * 100; }
|
||||
int move(int dx) const override { return x + dx; }
|
||||
};
|
||||
extern "C" int main(void) {
|
||||
Sprite s(7);
|
||||
Drawable *d = &s;
|
||||
Movable *m = &s;
|
||||
int ok = 0;
|
||||
if (d->draw() == 700) ok |= 1;
|
||||
if (m->move(5) == 12) ok |= 2;
|
||||
if (s.draw() == 700) ok |= 4;
|
||||
switchToBank2();
|
||||
*(volatile unsigned short *)0x5000 = (unsigned short)ok;
|
||||
while (1) {}
|
||||
}
|
||||
EOF
|
||||
"$PROJECT_ROOT/tools/llvm-mos-build/bin/clang++" --target=w65816 -O2 \
|
||||
-ffunction-sections -fno-exceptions -fno-rtti \
|
||||
-c "$cppMiFile" -o "$oCppMiFile"
|
||||
"$PROJECT_ROOT/tools/link816" -o "$binCppMiFile" --text-base 0x1000 \
|
||||
"$oCrt0F" "$oLibgccFile" "$oCppMiFile" \
|
||||
>/dev/null 2>&1
|
||||
if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binCppMiFile" --check \
|
||||
0x025000=0007 >/dev/null 2>&1; then
|
||||
die "MAME: C++ multiple inheritance != 0x07 (this-adjustment regression)"
|
||||
fi
|
||||
rm -f "$cppMiFile" "$oCppMiFile" "$binCppMiFile"
|
||||
|
||||
# C++ virtual base diamond: A and B both virtually inherit from
|
||||
# Base; Diamond inherits from both. There must be exactly one
|
||||
# Base subobject in Diamond (`d.b == 42`), and calls through
|
||||
# either A* or B* must reach Diamond's override (vbase offset
|
||||
# tables in the vtables resolve the shared subobject).
|
||||
log "check: MAME runs C++ virtual base diamond"
|
||||
cppVbFile="$(mktemp --suffix=.cpp)"
|
||||
oCppVbFile="$(mktemp --suffix=.o)"
|
||||
binCppVbFile="$(mktemp --suffix=.bin)"
|
||||
cat > "$cppVbFile" <<'EOF'
|
||||
extern "C" __attribute__((noinline)) void switchToBank2(void) {
|
||||
__asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n");
|
||||
}
|
||||
class Base {
|
||||
public:
|
||||
int b;
|
||||
Base(int x) : b(x) {}
|
||||
virtual int kind() const = 0;
|
||||
virtual ~Base() {}
|
||||
};
|
||||
class A : public virtual Base { public: A(int x) : Base(x) {} int kind() const override { return 1; } };
|
||||
class B : public virtual Base { public: B(int x) : Base(x) {} int kind() const override { return 2; } };
|
||||
class Diamond : public A, public B {
|
||||
public:
|
||||
Diamond(int x) : Base(x), A(x), B(x) {}
|
||||
int kind() const override { return 99; }
|
||||
};
|
||||
extern "C" int main(void) {
|
||||
Diamond d(42);
|
||||
int ok = 0;
|
||||
if (d.kind() == 99) ok |= 1;
|
||||
if (d.b == 42) ok |= 2;
|
||||
A *a = &d;
|
||||
B *b = &d;
|
||||
if (a->kind() == 99) ok |= 4;
|
||||
if (b->kind() == 99) ok |= 8;
|
||||
if (a->b == 42) ok |= 0x10;
|
||||
if (b->b == 42) ok |= 0x20;
|
||||
switchToBank2();
|
||||
*(volatile unsigned short *)0x5000 = (unsigned short)ok;
|
||||
while (1) {}
|
||||
}
|
||||
EOF
|
||||
"$PROJECT_ROOT/tools/llvm-mos-build/bin/clang++" --target=w65816 -O2 \
|
||||
-ffunction-sections -fno-exceptions -fno-rtti \
|
||||
-c "$cppVbFile" -o "$oCppVbFile"
|
||||
"$PROJECT_ROOT/tools/link816" -o "$binCppVbFile" --text-base 0x1000 \
|
||||
"$oCrt0F" "$oLibgccFile" "$oCppVbFile" \
|
||||
>/dev/null 2>&1
|
||||
if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binCppVbFile" --check \
|
||||
0x025000=003f >/dev/null 2>&1; then
|
||||
die "MAME: C++ virtual base diamond != 0x3F (vbase offset regression)"
|
||||
fi
|
||||
rm -f "$cppVbFile" "$oCppVbFile" "$binCppVbFile"
|
||||
|
||||
# Real-world: hex dumper using memory-backed file I/O. Reads
|
||||
# 16 bytes from a registered "in" file, writes a hex+ASCII
|
||||
# dump to a registered "out" file via fprintf. Verifies the
|
||||
|
|
@ -3976,6 +4079,162 @@ EOF
|
|||
fi
|
||||
rm -f "$cJsFile" "$oJsFile" "$binJsFile"
|
||||
|
||||
# Real-world: command-driven hash-table shell. ~250 lines
|
||||
# of C exercising malloc/free, hash table with chaining,
|
||||
# tokenizer state machine, mfsRegister + fopen/fprintf,
|
||||
# and ~10 sequential commands processed against the table.
|
||||
# Catches integration bugs that microbenchmarks miss.
|
||||
log "check: MAME runs hash-table shell (script + memory I/O)"
|
||||
cShFile="$(mktemp --suffix=.c)"
|
||||
oShFile="$(mktemp --suffix=.o)"
|
||||
binShFile="$(mktemp --suffix=.bin)"
|
||||
cat > "$cShFile" <<'EOF'
|
||||
extern void *malloc(unsigned int n);
|
||||
extern void free(void *p);
|
||||
extern unsigned int strlen(const char *s);
|
||||
extern int strcmp(const char *a, const char *b);
|
||||
extern char *strchr(const char *s, int c);
|
||||
extern char *strstr(const char *h, const char *n);
|
||||
extern int mfsRegister(const char *path, void *buf, unsigned int size, unsigned int cap, int writable);
|
||||
extern struct __sFILE *fopen(const char *path, const char *mode);
|
||||
extern int fclose(struct __sFILE *f);
|
||||
extern int fprintf(struct __sFILE *f, const char *fmt, ...);
|
||||
__attribute__((noinline)) static void switchToBank2(void) {
|
||||
__asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n");
|
||||
}
|
||||
__attribute__((noinline)) static char *strdup_(const char *s) {
|
||||
unsigned int n = strlen(s) + 1;
|
||||
char *r = (char *)malloc(n);
|
||||
if (r) for (unsigned int i = 0; i < n; i++) r[i] = s[i];
|
||||
return r;
|
||||
}
|
||||
__attribute__((noinline)) static unsigned short hashKey(const char *s) {
|
||||
unsigned short h = 5381;
|
||||
while (*s) { h = ((h << 5) + h) + (unsigned char)*s; s++; }
|
||||
return h;
|
||||
}
|
||||
#define HASH_BUCKETS 4
|
||||
typedef struct Entry { char *key; char *val; struct Entry *next; } Entry;
|
||||
static Entry *table[HASH_BUCKETS];
|
||||
static unsigned short totalEntries = 0;
|
||||
__attribute__((noinline)) static int dbInsert(const char *k, const char *v) {
|
||||
unsigned short h = hashKey(k) % HASH_BUCKETS;
|
||||
Entry *e = table[h];
|
||||
while (e) { if (strcmp(e->key, k) == 0) { free(e->val); e->val = strdup_(v); return 0; } e = e->next; }
|
||||
Entry *n = (Entry *)malloc(sizeof(Entry));
|
||||
if (!n) return -1;
|
||||
n->key = strdup_(k); n->val = strdup_(v); n->next = table[h]; table[h] = n;
|
||||
totalEntries++; return 1;
|
||||
}
|
||||
__attribute__((noinline)) static const char *dbGet(const char *k) {
|
||||
Entry *e = table[hashKey(k) % HASH_BUCKETS];
|
||||
while (e) { if (strcmp(e->key, k) == 0) return e->val; e = e->next; }
|
||||
return (const char *)0;
|
||||
}
|
||||
__attribute__((noinline)) static int dbDelete(const char *k) {
|
||||
unsigned short h = hashKey(k) % HASH_BUCKETS;
|
||||
Entry *e = table[h]; Entry **pp = &table[h];
|
||||
while (e) {
|
||||
if (strcmp(e->key, k) == 0) { *pp = e->next; free(e->key); free(e->val); free(e); totalEntries--; return 1; }
|
||||
pp = &e->next; e = e->next;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static char *skipWs(char *s) { while (*s == ' ' || *s == '\t') s++; return s; }
|
||||
__attribute__((noinline)) static char *takeToken(char *s, char **out) {
|
||||
s = skipWs(s);
|
||||
if (!*s) { *out = (char *)0; return s; }
|
||||
*out = s;
|
||||
while (*s && *s != ' ' && *s != '\t') s++;
|
||||
if (*s) { *s = 0; s++; }
|
||||
return s;
|
||||
}
|
||||
__attribute__((noinline)) static char *takeRest(char *s) {
|
||||
s = skipWs(s);
|
||||
char *end = s + strlen(s);
|
||||
while (end > s && (end[-1] == ' ' || end[-1] == '\t')) end--;
|
||||
*end = 0;
|
||||
return *s ? s : (char *)0;
|
||||
}
|
||||
__attribute__((noinline)) static int dispatch(char *line, struct __sFILE *out) {
|
||||
char *cmd; char *rest = takeToken(line, &cmd);
|
||||
if (!cmd) return 0;
|
||||
if (strcmp(cmd, "INSERT") == 0) {
|
||||
char *key; char *r2 = takeToken(rest, &key); char *val = takeRest(r2);
|
||||
if (key && val) {
|
||||
int rc = dbInsert(key, val);
|
||||
fprintf(out, "INSERT %s = %s -> %s\n", key, val,
|
||||
rc == 1 ? "added" : (rc == 0 ? "updated" : "fail"));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(cmd, "GET") == 0) {
|
||||
char *key = takeRest(rest);
|
||||
if (key) { const char *v = dbGet(key); fprintf(out, "GET %s = %s\n", key, v ? v : "(none)"); }
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(cmd, "DELETE") == 0) {
|
||||
char *key = takeRest(rest);
|
||||
if (key) { int rc = dbDelete(key); fprintf(out, "DELETE %s -> %s\n", key, rc ? "removed" : "not found"); }
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(cmd, "COUNT") == 0) { fprintf(out, "COUNT = %u\n", (unsigned)totalEntries); return 1; }
|
||||
return 0;
|
||||
}
|
||||
__attribute__((noinline)) static int runScript(const char *script, struct __sFILE *out) {
|
||||
int n = 0;
|
||||
char buf[64];
|
||||
const char *p = script;
|
||||
while (*p) {
|
||||
const char *eol = strchr(p, '\n');
|
||||
unsigned int len = eol ? (unsigned int)(eol - p) : strlen(p);
|
||||
if (len >= sizeof(buf)) len = sizeof(buf) - 1;
|
||||
for (unsigned int i = 0; i < len; i++) buf[i] = p[i];
|
||||
buf[len] = 0;
|
||||
n += dispatch(buf, out);
|
||||
p += len; if (*p == '\n') p++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
static char outbuf[1024];
|
||||
static const char SCRIPT[] =
|
||||
"INSERT name alice\n" "INSERT age 30\n"
|
||||
"GET name\n" "INSERT name bob\n" "GET name\n"
|
||||
"GET nope\n" "COUNT\n" "DELETE age\n"
|
||||
"DELETE age\n" "COUNT\n";
|
||||
int main(void) {
|
||||
mfsRegister("out", outbuf, 0, 1024, 1);
|
||||
struct __sFILE *out = fopen("out", "w");
|
||||
int cmds = runScript(SCRIPT, out);
|
||||
fprintf(out, "ran %d cmds\n", cmds);
|
||||
fclose(out);
|
||||
int ok = 0;
|
||||
if (strstr(outbuf, "INSERT name = alice -> added")) ok |= 0x001;
|
||||
if (strstr(outbuf, "INSERT name = bob -> updated")) ok |= 0x002;
|
||||
if (strstr(outbuf, "GET name = bob")) ok |= 0x004;
|
||||
if (strstr(outbuf, "GET nope = (none)")) ok |= 0x008;
|
||||
if (strstr(outbuf, "DELETE age -> removed")) ok |= 0x010;
|
||||
if (strstr(outbuf, "DELETE age -> not found")) ok |= 0x020;
|
||||
if (strstr(outbuf, "COUNT = 2")) ok |= 0x040;
|
||||
if (strstr(outbuf, "COUNT = 1")) ok |= 0x080;
|
||||
if (strstr(outbuf, "ran 10 cmds")) ok |= 0x100;
|
||||
switchToBank2();
|
||||
*(volatile unsigned short *)0x5000 = (unsigned short)ok;
|
||||
while (1) {}
|
||||
}
|
||||
EOF
|
||||
"$CLANG" --target=w65816 -O2 -ffunction-sections -c \
|
||||
"$cShFile" -o "$oShFile"
|
||||
"$PROJECT_ROOT/tools/link816" -o "$binShFile" --text-base 0x1000 \
|
||||
"$oCrt0F" "$oLibcF" "$oExtrasF" "$oSnprintfF" \
|
||||
"$oSfF" "$oSdF" "$oLibgccFile" "$oShFile" \
|
||||
>/dev/null 2>&1
|
||||
if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binShFile" --check \
|
||||
0x025000=01ff >/dev/null 2>&1; then
|
||||
die "MAME: hash-table shell bitmap != 0x1FF"
|
||||
fi
|
||||
rm -f "$cShFile" "$oShFile" "$binShFile"
|
||||
|
||||
rm -f "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \
|
||||
"$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" "$oCrt0F"
|
||||
else
|
||||
|
|
@ -4078,6 +4337,56 @@ EOF
|
|||
fi
|
||||
rm -f "$oToolFile" "$oToolboxAsm" "$binTbx"
|
||||
|
||||
# iigs/gsos.h — GS/OS class-1 dispatch wrappers for fopen/etc.
|
||||
# The wrappers can't be exercised in MAME (no ProDOS volume in
|
||||
# the smoke harness), but we verify the header compiles and
|
||||
# the .s wrappers link. Build runtime objs locally since the
|
||||
# MAME-block ones above were already rm'd.
|
||||
log "check: iigs/gsos.h + iigsGsos.s compile and link"
|
||||
cGsFile="$(mktemp --suffix=.c)"
|
||||
oGsFile="$(mktemp --suffix=.o)"
|
||||
oGsAsm="$(mktemp --suffix=.o)"
|
||||
oGsLibc="$(mktemp --suffix=.o)"
|
||||
oGsSnp="$(mktemp --suffix=.o)"
|
||||
oGsSf="$(mktemp --suffix=.o)"
|
||||
oGsSd="$(mktemp --suffix=.o)"
|
||||
binGs="$(mktemp --suffix=.bin)"
|
||||
cat > "$cGsFile" <<'EOF'
|
||||
#include <iigs/gsos.h>
|
||||
int main(void) {
|
||||
GSString *p = (GSString *)0x4000;
|
||||
OpenParm op = { 2, 0, p };
|
||||
if (gsosOpen(&op) != 0) return 1;
|
||||
static char buf[64];
|
||||
IORecGS r = { 4, op.refNum, buf, 64, 0 };
|
||||
if (gsosRead(&r) != 0) return 2;
|
||||
RefNumRecGS c = { 1, op.refNum };
|
||||
return gsosClose(&c);
|
||||
}
|
||||
EOF
|
||||
"$CLANG" --target=w65816 -O2 -I"$PROJECT_ROOT/runtime/include" -ffunction-sections \
|
||||
-c "$cGsFile" -o "$oGsFile"
|
||||
"$PROJECT_ROOT/tools/llvm-mos-build/bin/llvm-mc" -arch=w65816 -filetype=obj \
|
||||
"$PROJECT_ROOT/runtime/src/iigsGsos.s" -o "$oGsAsm"
|
||||
"$CLANG" --target=w65816 -O2 -ffunction-sections \
|
||||
-c "$PROJECT_ROOT/runtime/src/libc.c" -o "$oGsLibc" 2>/dev/null
|
||||
"$CLANG" --target=w65816 -O2 -ffunction-sections \
|
||||
-c "$PROJECT_ROOT/runtime/src/snprintf.c" -o "$oGsSnp" 2>/dev/null
|
||||
"$CLANG" --target=w65816 -O2 -ffunction-sections \
|
||||
-c "$PROJECT_ROOT/runtime/src/softFloat.c" -o "$oGsSf" 2>/dev/null
|
||||
"$CLANG" --target=w65816 -O2 -ffunction-sections \
|
||||
-c "$PROJECT_ROOT/runtime/src/softDouble.c" -o "$oGsSd" 2>/dev/null
|
||||
oGsCrt0="$(mktemp --suffix=.o)"
|
||||
"$PROJECT_ROOT/tools/llvm-mos-build/bin/llvm-mc" -arch=w65816 -filetype=obj \
|
||||
"$PROJECT_ROOT/runtime/src/crt0.s" -o "$oGsCrt0"
|
||||
if ! "$PROJECT_ROOT/tools/link816" -o "$binGs" --text-base 0x1000 \
|
||||
"$oGsCrt0" "$oGsLibc" "$oGsSnp" "$oGsSf" "$oGsSd" \
|
||||
"$oGsFile" "$oGsAsm" "$oLibgccFile" \
|
||||
--no-gc-sections 2>&1; then
|
||||
die "iigs/gsos.h + iigsGsos.s failed to link"
|
||||
fi
|
||||
rm -f "$cGsFile" "$oGsFile" "$oGsAsm" "$oGsLibc" "$oGsSnp" "$oGsSf" "$oGsSd" "$oGsCrt0" "$binGs"
|
||||
|
||||
# stdint.h / stddef.h / limits.h / inttypes.h: standalone
|
||||
# replacements for clang's bundled versions (which try to include
|
||||
# glibc bits/* headers and break the build). Compile a small
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue