From deb1c96f73db4f6f99cd764ee4f51e6b6334a8e5 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 28 Sep 2026 21:04:28 -0500 Subject: [PATCH] Far pointer code is now more efficient. More code gen errors fixed. --- demos/gnoBigBssProbe.c | 16 +- demos/rsrcProbe.apl | Bin 46741 -> 47302 bytes runtime/src/crt0Gno.s | 17 +- runtime/src/crt0Gsos.s | 77 +- runtime/src/libc.c | 57 +- runtime/src/libgcc.s | 23 + scripts/runFileCheckTests.sh | 9 +- scripts/smokeTest.sh | 62 + src/link816/link816.cpp | 55 +- src/llvm/lib/Target/W65816/CMakeLists.txt | 1 + src/llvm/lib/Target/W65816/W65816.h | 7 + .../Target/W65816/W65816FarPtrStageReuse.cpp | 1237 +++++++++++++++++ .../lib/Target/W65816/W65816ISelLowering.cpp | 29 +- .../lib/Target/W65816/W65816InstrInfo.cpp | 18 +- src/llvm/lib/Target/W65816/W65816InstrInfo.td | 12 + .../Target/W65816/W65816StackSlotCleanup.cpp | 30 +- .../lib/Target/W65816/W65816TargetMachine.cpp | 5 + .../CodeGen/W65816/far-ptr-stage-reuse.mir | 465 +++++++ .../W65816/srl8a-flag-corrupt-wrap.mir | 112 ++ .../CodeGen/W65816/volatile-byte-load-8bit.ll | 76 + tests/coremark/README.md | 11 +- tests/coremark/coreMark.manifest.json | 27 + tests/coremark/runCoreMark.sh | 16 +- tests/lua/README.md | 22 +- tests/lua/luaTest.manifest.json | 22 +- 25 files changed, 2273 insertions(+), 133 deletions(-) create mode 100644 src/llvm/lib/Target/W65816/W65816FarPtrStageReuse.cpp create mode 100644 src/llvm/test/CodeGen/W65816/far-ptr-stage-reuse.mir create mode 100644 src/llvm/test/CodeGen/W65816/srl8a-flag-corrupt-wrap.mir create mode 100644 src/llvm/test/CodeGen/W65816/volatile-byte-load-8bit.ll create mode 100644 tests/coremark/coreMark.manifest.json diff --git a/demos/gnoBigBssProbe.c b/demos/gnoBigBssProbe.c index 6723c95..538ec76 100644 --- a/demos/gnoBigBssProbe.c +++ b/demos/gnoBigBssProbe.c @@ -4,24 +4,26 @@ // the program must run to the end AND read the array back as all-zero. #include -// ~5.75 KB. With the current GNO libc the runtime BSS base already sits at -// ~0xD000 (well above the old ~0xB100 heartbeat threshold), so this array's job -// is to keep __bss_end large (near the bank-0 top) while still fitting bank 0. -static volatile uint8_t bigBss[0x1700]; +// 4 KB. The GNO runtime's code already pushes the BSS base to ~0xD000 (well +// above the old ~0xB100 heartbeat threshold), so this array only has to keep +// __bss_end large while the whole BSS still fits under the bank-0 top: the +// runtime's own BSS grows, and at 0x1700 the link overflowed by 236 bytes. +#define BIG_BSS_BYTES 0x1000u +static volatile uint8_t bigBss[BIG_BSS_BYTES]; int main(void) { *(volatile uint16_t *)0x025010UL = 0xBEEF; uint16_t zeroed = 1; unsigned i; - for (i = 0; i < 0x1700; i++) { + for (i = 0; i < BIG_BSS_BYTES; i++) { if (bigBss[i] != 0) { zeroed = 0; break; } } *(volatile uint16_t *)0x025012UL = zeroed ? 0x0001 : 0xDEAD; - bigBss[0] = 1; // retain - bigBss[0x16FF] = 2; + bigBss[0] = 1; // retain + bigBss[BIG_BSS_BYTES - 1u] = 2; *(volatile uint16_t *)0x025000UL = 0xC0DE; for (volatile unsigned long j = 0; j < 600000UL; j++) { } diff --git a/demos/rsrcProbe.apl b/demos/rsrcProbe.apl index 3fe541a8dcf4815eb4e96d28878dbe2b87061bfc..79f5525e17c35b8a9b7424ef34ee04be8e64fed4 100644 GIT binary patch delta 12352 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

tAQC~Q=C{Yz>P=-D)@3;3}1*F6@@dL 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 diff --git a/runtime/src/crt0Gno.s b/runtime/src/crt0Gno.s index 257f9b8..976ff4d 100644 --- a/runtime/src/crt0Gno.s +++ b/runtime/src/crt0Gno.s @@ -165,15 +165,10 @@ __start: ; 3. returns whatever main returned. jsl __gnoStartup - ; Run global dtors registered via __cxa_atexit (LIFO order) before - ; QUIT. A holds main()'s return value (exit status), which _exit - ; consumes — preserve it across the call. - pha - jsl __run_cxa_atexit - pla - - ; ---- Terminate via _exit (libcGno.c) ---- - ; __gnoStartup returned with A = main()'s exit status. _exit issues + ; ---- Terminate via exit -> _exit (libcGno.c) ---- + ; __gnoStartup returned with A = main()'s exit status. exit() (libc.c) + ; runs the atexit handlers, then the C++ dtors registered through + ; __cxa_atexit, then _exit, which issues ; GS/OS QUIT through GNO's INLINE dispatch (__gnoGsosCall), which is ; REQUIRED under GNO: the old stack-based QUIT here (ldx #$2029 ; ; jsl $e100a8 with the pblock pushed) was misread by GNO's OurGSOS @@ -181,9 +176,9 @@ __start: ; bytes after the jsl, not the stack — so the QUIT was mis-dispatched, ; the process was left restartable, and GNO RE-RAN main() a second ; time (silently draining redirected stdin on the first pass). - jsl _exit + jsl exit - ; _exit does not return. If it ever does, spin rather than fall + ; exit does not return. If it ever does, spin rather than fall ; into a BRK (headless MAME mis-vectors BRK to $0000, which looks ; like a wild crash) or off the end of the segment. .Lhang: diff --git a/runtime/src/crt0Gsos.s b/runtime/src/crt0Gsos.s index c059870..ed872c1 100644 --- a/runtime/src/crt0Gsos.s +++ b/runtime/src/crt0Gsos.s @@ -6,9 +6,10 @@ ; - No language-card RAM enable (GS/OS configures memory). ; - No stack base reset (GS/OS allocated and set our SP). ; - Honors GS/OS's DBR=our-bank, DP=allocated-page setup. -; - On main() return, calls GS/OS QUIT(pcount=0) to return control to -; the launching program (Finder / shell / boot chain) — the standard -; GS/OS application exit. +; - On main() return, calls exit(), which runs the atexit handlers and +; C++ dtors and then _exit (below): GS/OS QUIT(pcount=0), returning +; control to the launching program (Finder / shell / boot chain) -- +; the standard GS/OS application exit. ; ; Entry from the System Loader (per Apple IIgs Toolbox Reference): ; E=0 (native), M=0 (16-bit accumulator), X=0 (16-bit index) @@ -154,35 +155,51 @@ __start: rep #0x30 jsl main - ; Run global dtors registered via __cxa_atexit (LIFO order) before - ; QUIT. A holds main()'s return value; QUIT itself doesn't consume - ; it (we set up the parm block immediately below), but keep the - ; save/restore for parity with crt0Gno. - pha - jsl __run_cxa_atexit - pla + ; Returning from main is exit(main's value) (C99 5.1.2.2.3): exit runs + ; the atexit handlers, then the C++ static dtors registered through + ; __cxa_atexit, then _exit below. A holds main()'s return value, which + ; is exit's argument. + jsl exit - ; ---- QUIT (pcount=0): return to the launching program ---------- - ; The standard GS/OS application exit. A pcount=0 parm block (just - ; a zero pcount word) tells QUIT to return control to whoever - ; launched us (Finder, a shell, the boot chain) with default flags - ; (non-restartable) — no hardcoded chain path needed. A program - ; that wants to chain elsewhere can provide its own _exit/crt0. - rep #0x30 - stz 0x80 ; pcount = 0 (parm block at DP $0080, bank 0) - tdc - clc - adc #0x80 - pha ; parm-block ptr: low 16 (offset) - pea 0 ; parm-block ptr: high 16 (bank 0) - ldx #0x2029 ; QUIT class-1 call number - jsl 0xe100a8 ; GS/OS dispatcher - - ; QUIT only returns on failure. Clean up the pushed pointer and - ; spin (don't fall into a BRK, which headless MAME mis-vectors). - pla - pla + ; exit does not return. Spin rather than fall into what follows. .Lhang: bra .Lhang .size __start, . - __start + +; --------------------------------------------------------------------- +; void _exit(int status) -- GS/OS QUIT (pcount=0): return control to +; whoever launched us (Finder, a shell, the boot chain) with default +; flags (non-restartable). The status is not passed on. +; +; GS/OS takes class-1 calls in the INLINE form (see iigsGsos.s): the call +; number and the parm-block pointer follow the jsl, and the dispatcher +; returns past them. This used to push the pointer and load the call +; number into X for the same $E100A8 entry, so GS/OS read the code after +; the jsl as the call, failed, and returned into the middle of the next +; instructions -- every program that returned from main crashed into the +; monitor (JoeyLib's LLVM816-ASKS #6). The parm block is a zero pcount +; word at DP $80 (bank 0); its address is patched into the inline slot, +; like gsosDispatch does. +; --------------------------------------------------------------------- + .globl _exit +_exit: + rep #0x30 + phk + plb ; DBR := PBR: the stores below patch this code + stz 0x80 ; pcount = 0 + tdc + clc + adc #0x80 + sta .Lquit_pb ; parm-block pointer, low 16 + stz .Lquit_pb+2 ; bank 0, pad 0 + jsl 0xe100a8 + .word 0x2029 ; QUIT +.Lquit_pb: + .long 0 + ; QUIT only returns on failure. Spin (a BRK is mis-vectored by + ; headless MAME and reads as a wild crash). +.Lquit_failed: + bra .Lquit_failed + + .size _exit, . - _exit diff --git a/runtime/src/libc.c b/runtime/src/libc.c index 0e5593f..9df4b97 100644 --- a/runtime/src/libc.c +++ b/runtime/src/libc.c @@ -806,33 +806,33 @@ void *realloc(void *ptr, size_t n) { // ---- atexit / exit ---- // -// Standard exit() halts via BRK after running any registered atexit -// handler. Programs running under the IIgs runtime typically would -// call back into GS/OS Quit; here we just wedge the CPU. Single-slot -// atexit (the storage and registration function are below). +// exit() runs the atexit handlers (last registered first), then the C++ +// static destructors registered through __cxa_atexit, then _exit. +// Returning from main is the same exit: the GS/OS and GNO crt0s call exit +// with main's value. _exit is the platform's way out -- crt0Gsos.s issues +// GS/OS QUIT, libcGno.c GNO's QUIT, and the weak fallback in libgcc.s +// halts bare metal (BRK so MAME's debugger catches it, then spin). typedef void (*AtexitFn)(void); -static AtexitFn __atexitFn = (AtexitFn)0; -// BRK $00 then spin -- halts a 65816 in BRK so MAME's debugger catches -// it; the spin loop guards against the (rare) case where BRK returns. -static void __halt(void) __attribute__((noreturn)); -static void __halt(void) { - __asm__ volatile (".byte 0x00, 0x00"); - while (1) {} -} +// C99 7.20.4.2 promises at least 32 registrations. +#define ATEXIT_MAX 32 +static AtexitFn __atexitFns[ATEXIT_MAX]; +static unsigned int __atexitCount = 0; + +void _exit(int code) __attribute__((noreturn)); +void __run_cxa_atexit(void); void exit(int code) { - (void)code; - // C99 7.20.4.3: exit() must invoke registered atexit handlers in - // reverse-registration order before terminating. - if (__atexitFn) { - AtexitFn fn = __atexitFn; - __atexitFn = (AtexitFn)0; // prevent re-entry if fn calls exit - fn(); + // Pop before calling, so a handler that registers another one has it + // run next and none runs twice. + while (__atexitCount > 0) { + __atexitCount--; + __atexitFns[__atexitCount](); } - __halt(); + __run_cxa_atexit(); + _exit(code); } // ---- errno ---- @@ -1447,10 +1447,11 @@ void abort(void) { exit(127); } -// ---- atexit (single slot; storage + exit() invocation above) ---- +// ---- atexit (storage + exit() above) ---- int atexit(AtexitFn fn) { - if (__atexitFn) return -1; - __atexitFn = fn; + if (__atexitCount >= ATEXIT_MAX) return -1; + __atexitFns[__atexitCount] = fn; + __atexitCount++; return 0; } @@ -1458,24 +1459,22 @@ int atexit(AtexitFn fn) { // // _Exit terminates without invoking atexit handlers (unlike exit). // quick_exit terminates after invoking at_quick_exit handlers (a -// separate chain from atexit). We share the single-slot pattern -// with atexit — single-shot handler, second registration fails. +// separate chain from atexit). at_quick_exit keeps a single slot; +// a second registration fails. static AtexitFn __quickFn = (AtexitFn)0; void _Exit(int code) { - (void)code; - __halt(); + _exit(code); } void quick_exit(int code) { - (void)code; if (__quickFn) { AtexitFn fn = __quickFn; __quickFn = (AtexitFn)0; fn(); } - __halt(); + _exit(code); } int at_quick_exit(AtexitFn fn) { diff --git a/runtime/src/libgcc.s b/runtime/src/libgcc.s index 7d390c8..9cacdef 100644 --- a/runtime/src/libgcc.s +++ b/runtime/src/libgcc.s @@ -100,6 +100,29 @@ __jsl_indir: __run_cxa_atexit: rtl +; -------------------------------------------------------------------- +; exit / _exit -- weak fallbacks. +; +; crt0Gsos.s and crt0Gno.s end a program by calling exit() with main()'s +; value in A. libc.c has the real exit (atexit handlers, then C++ dtors, +; then _exit); this weak one serves programs that link no libc: dtors, +; then _exit. _exit is the platform's way out: crt0Gsos.s (GS/OS QUIT) +; and libcGno.c (GNO QUIT) override the weak halt below, which is bare +; metal's -- BRK so MAME's debugger catches it, then spin. +; -------------------------------------------------------------------- + .weak exit +exit: + pha ; status + jsl __run_cxa_atexit + pla + jsl _exit ; does not return + + .weak _exit +_exit: + .byte 0x00, 0x00 ; BRK +.L_exitSpin: + bra .L_exitSpin + ; -------------------------------------------------------------------- ; __heapLock / __heapUnlock -- weak no-op heap-serialization hooks. ; diff --git a/scripts/runFileCheckTests.sh b/scripts/runFileCheckTests.sh index 0c0e107..04842c0 100755 --- a/scripts/runFileCheckTests.sh +++ b/scripts/runFileCheckTests.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash # runFileCheckTests.sh - run W65816 backend regression tests. # -# Walks src/llvm/test/CodeGen/W65816/*.ll and for each: -# - reads RUN: lines from the test header (lit-compatible syntax) +# Walks src/llvm/test/CodeGen/W65816/*.ll and *.mir and for each: +# - reads RUN: lines from the test header (lit-compatible syntax; `;` +# comments in .ll files, `#` comments in .mir files) # - executes them with %s -> the test path # - any non-zero exit fails the run. # @@ -32,7 +33,7 @@ if [ $# -gt 0 ]; then files+=("$TEST_DIR/$f") done else - mapfile -t files < <(find "$TEST_DIR" -maxdepth 1 -name '*.ll' | sort) + mapfile -t files < <(find "$TEST_DIR" -maxdepth 1 \( -name '*.ll' -o -name '*.mir' \) | sort) fi pass=0 @@ -42,7 +43,7 @@ for f in "${files[@]}"; do [ -f "$f" ] || { echo "skip missing: $f"; continue; } name="$(basename "$f")" - runs=$(grep -E '^[[:space:]]*;[[:space:]]*RUN:' "$f" | sed -E 's/^[[:space:]]*;[[:space:]]*RUN:[[:space:]]*//') + runs=$(grep -E '^[[:space:]]*[;#][[:space:]]*RUN:' "$f" | sed -E 's/^[[:space:]]*[;#][[:space:]]*RUN:[[:space:]]*//') if [ -z "$runs" ]; then echo "SKIP $name (no RUN: line)" continue diff --git a/scripts/smokeTest.sh b/scripts/smokeTest.sh index f903868..247a36c 100755 --- a/scripts/smokeTest.sh +++ b/scripts/smokeTest.sh @@ -6713,6 +6713,68 @@ EOF "$omfGsf" "$testFileGsf" fi +# GS/OS program exit: returning from main is exit(main's value), which must +# run the atexit handlers (last registered first), then the C++ dtors, then +# QUIT back to the Finder. crt0Gsos used to issue QUIT with the pointer +# pushed onto the stack at the INLINE entry $E100A8, so GS/OS misread the +# call and returned into the middle of the following code: every program +# that returned from main crashed into the monitor (text mode), and the +# atexit handlers never ran at all. Three handlers fold their ids into $70 +# (LIFO gives 0x39); main marks $71; NEWVIDEO ($C029) reads 0xC1 only if +# the Finder's Super Hi-Res desktop is back (the monitor leaves 0x01). +# Skips under SMOKE_SKIP_GSOSEXIT=1. +if [ "${SMOKE_SKIP_GSOSEXIT:-0}" != "1" ] \ + && [ -x "$CLANG" ] && [ -x "$CADIUS" ] && [ -f "$SYSDISK" ] \ + && command -v mame >/dev/null 2>&1; then + log "check: GS/OS exit runs atexit handlers LIFO and QUITs to the Finder" + cGsx="$(mktemp --suffix=.c)" + oGsx="$(mktemp --suffix=.o)" + binGsx="$(mktemp --suffix=.bin)" + mapGsx="$(mktemp --suffix=.map)" + relGsx="$(mktemp --suffix=.reloc)" + omfGsx="$(mktemp --suffix=.omf)" + cat > "$cGsx" <<'EOF' +#include +#define MB ((volatile unsigned char *)0x70) +static void a(void) { MB[0] = (unsigned char)((MB[0] << 2) | 1u); } +static void b(void) { MB[0] = (unsigned char)((MB[0] << 2) | 2u); } +static void c(void) { MB[0] = (unsigned char)((MB[0] << 2) | 3u); } +int main(void) { + MB[0] = 0u; + atexit(a); + atexit(b); + atexit(c); + MB[1] = 0xA5u; + return 0; +} +EOF + "$CLANG" --target=w65816 -I"$PROJECT_ROOT/runtime/include" -O2 -ffunction-sections -c \ + "$cGsx" -o "$oGsx" + "$PROJECT_ROOT/tools/link816" -o "$binGsx" --text-base 0x1000 \ + --map "$mapGsx" --reloc-out "$relGsx" \ + "$PROJECT_ROOT/runtime/crt0Gsos.o" "$oGsx" \ + "$PROJECT_ROOT/runtime/libc.o" \ + "$PROJECT_ROOT/runtime/snprintf.o" \ + "$PROJECT_ROOT/runtime/extras.o" \ + "$PROJECT_ROOT/runtime/softFloat.o" \ + "$PROJECT_ROOT/runtime/softDouble.o" \ + "$PROJECT_ROOT/runtime/iigsGsos.o" \ + "$PROJECT_ROOT/runtime/iigsToolbox.o" "$PROJECT_ROOT/runtime/iigsToolboxExtra.o" \ + "$PROJECT_ROOT/runtime/libgcc.o" >/dev/null \ + || die "GS/OS exit smoke: link failed" + "$PROJECT_ROOT/tools/omfEmit" --input "$binGsx" --map "$mapGsx" \ + --base 0x1000 --entry __start --output "$omfGsx" \ + --name HELLO --expressload --relocs "$relGsx" >/dev/null 2>&1 + [ -s "$omfGsx" ] || die "GS/OS exit smoke: omfEmit produced empty/missing OMF" + if ! bash "$PROJECT_ROOT/scripts/runViaFinder.sh" "$omfGsx" \ + --check 0x70=0x39 --check 0x71=0xa5 --check 0xc029=0xc1 >/dev/null 2>&1; then + bash "$PROJECT_ROOT/scripts/runViaFinder.sh" "$omfGsx" \ + --check 0x70=0x39 --check 0x71=0xa5 --check 0xc029=0xc1 2>&1 | tail -5 >&2 + die "GS/OS exit smoke FAILED (atexit order / QUIT to the Finder)" + fi + rm -f "$cGsx" "$oGsx" "$binGsx" "$mapGsx" "$relGsx" "$omfGsx" +fi + # Phase C.2: cINTERSEG cross-segment JSL under real GS/OS Loader. # Links a 2-user-seg program (main in seg 1, pinnedFn in seg 2 via # __attribute__((section(".text.seg2")))), uses --multi-seg-expressload diff --git a/src/link816/link816.cpp b/src/link816/link816.cpp index 7a6f651..518a9de 100644 --- a/src/link816/link816.cpp +++ b/src/link816/link816.cpp @@ -764,22 +764,7 @@ static void applyTextReloc(std::vector &buf, uint32_t off, break; } } - if (!gMultiSegRelocMode) { - applyReloc(buf, off, patchAddr, target, rtype, symName); - return; - } - if (patchSeg == 0 || patchSeg > segments.size()) { - applyReloc(buf, off, patchAddr, target, rtype, symName); - return; - } - const TextSeg &pseg = segments[patchSeg - 1]; uint32_t targetSeg = findSegContaining(target, segments); - gDbgTextCalls++; - // TEMP TRACE (RetroNet hunt): decisions for the mirror-pack refs. - if (symName.find("gRnInPack") != std::string::npos) { - std::fprintf(stderr, "TRACE %s rtype=%u patchSeg=%u targetSeg=%u target=0x%X rec=%d\n", - symName.c_str(), rtype, patchSeg, targetSeg, target, (int)gRecordSites); - } // Cross-segment FUNCTION POINTER (IMM16 &func to a function in seg2+): // clang forms the pointer as func's 16-bit offset + the $00BE seg1 bank, // which is wrong for a function in another bank. Redirect the IMM16 to @@ -790,6 +775,11 @@ static void applyTextReloc(std::vector &buf, uint32_t off, // non-seg1 bank) is the real key. No cRELOC: the baked value is a seg1 // offset and seg1 (KIND=0x1000) always bank-aligns, so the low16 is // already the runtime offset (matches the --text-base 0 IMM16 contract). + // Any multi-segment layout needs this, not just the GS/OS ExpressLoad + // one: a bare-metal --segment-cap link (fixed bank bases, no Loader) + // forms &func the same way, and without the trampoline a pointer to a + // seg2+ function is called in bank 0 (Lua's allocator, the first thing + // lua_newstate calls, ran into vfprintf and the machine went wild). if (rtype == R_W65816_IMM16 && targetSeg > 1) { uint32_t targetOff = target - segments[targetSeg - 1].base; auto tIt = gTrampMap.find(TrampKey{targetSeg, targetOff}); @@ -800,6 +790,16 @@ static void applyTextReloc(std::vector &buf, uint32_t off, return; } } + if (!gMultiSegRelocMode) { + applyReloc(buf, off, patchAddr, target, rtype, symName); + return; + } + if (patchSeg == 0 || patchSeg > segments.size()) { + applyReloc(buf, off, patchAddr, target, rtype, symName); + return; + } + const TextSeg &pseg = segments[patchSeg - 1]; + gDbgTextCalls++; // Non-text target (rodata / bss / init_array / weak-zero) — use // the legacy applyReloc path (which records via recordCRelocSite // into gImm24Sites for back-compat AND also pushes into the new @@ -821,11 +821,16 @@ static void applyTextReloc(std::vector &buf, uint32_t off, uint8_t cnt = (rtype == R_W65816_IMM24 || rtype == R_W65816_DATA32) ? 3 : 2; for (uint8_t i = 0; i < cnt; ++i) buf[off + i] = 0; + // The Loader adds seg 1's load address to an OFFSET within seg 1, + // so subtract seg 1's link base. (Passing the link address only + // worked at --text-base 0; at 0x1000 every such data reference + // landed 0x1000 past its variable -- smoke Phase C.2 read an + // initialised global as 0.) InterImm24Site s; s.patchSeg = static_cast(patchSeg); s.patchOff = static_cast(patchAddr - pseg.base); s.targetSeg = 1; - s.targetOff = static_cast(target); + s.targetOff = static_cast(target - segments[0].base); s.byteCnt = cnt; s.bitShift = (rtype == R_W65816_BANK16) ? 16 : 0; gInterImm24Sites.push_back(s); @@ -1370,7 +1375,7 @@ struct Linker { } } - // ---- TRAMPOLINE pre-pass (multi-seg only). Find every IMM16 + // ---- TRAMPOLINE pre-pass (multi-segment text only). Find every IMM16 // address-take of a function the layout placed in a text seg N>1 (a // different bank than seg1, whose bank is what $00BE/&func will use) // and reserve one `JML func` trampoline in seg1 per distinct target. @@ -1382,7 +1387,7 @@ struct Linker { gTrampMap.clear(); gTrampList.clear(); gTrampBase = 0; - if (gMultiSegRelocMode) { + if (segSizes.size() > 1) { std::map symSeg; // name -> (seg, off) std::map symStrong; for (size_t fi = 0; fi < objs.size(); ++fi) { @@ -1902,16 +1907,24 @@ struct Linker { } } // ---- Emit the cross-seg trampoline table at the end of seg 1's text. - // Each entry is `JML func` (0x5C + 24-bit target, zeroed); its cINTERSEG - // site makes the Loader patch the target to func's runtime bank:offset. + // Each entry is `JML func` (0x5C + 24-bit target). Under ExpressLoad + // the target is zeroed and its cINTERSEG site makes the Loader patch + // func's runtime bank:offset; a bare-metal link's segment bases are + // final, so the absolute target is baked in. // segTextBufs[0] holds exactly gTrampBase bytes here (all seg-1 text), // so entry i lands at offset gTrampBase + 4*i, and applyTextReloc // redirects each cross-seg IMM16 &func to that offset. if (!gTrampList.empty()) { auto &b0 = segTextBufs[0]; for (size_t i = 0; i < gTrampList.size(); ++i) { + uint32_t tgt = gMultiSegRelocMode + ? 0u + : L.segments[gTrampList[i].seg - 1].base + + gTrampList[i].off; b0.push_back(0x5C); // JML abs-long opcode - b0.push_back(0); b0.push_back(0); b0.push_back(0); // 24-bit tgt + b0.push_back(static_cast(tgt & 0xFF)); + b0.push_back(static_cast((tgt >> 8) & 0xFF)); + b0.push_back(static_cast((tgt >> 16) & 0xFF)); if (gRecordSites) { InterImm24Site s; s.patchSeg = 1; diff --git a/src/llvm/lib/Target/W65816/CMakeLists.txt b/src/llvm/lib/Target/W65816/CMakeLists.txt index 6356e11..9400e96 100644 --- a/src/llvm/lib/Target/W65816/CMakeLists.txt +++ b/src/llvm/lib/Target/W65816/CMakeLists.txt @@ -26,6 +26,7 @@ add_llvm_target(W65816CodeGen W65816Subtarget.cpp W65816StackSlotCleanup.cpp W65816SepRepCleanup.cpp + W65816FarPtrStageReuse.cpp W65816BranchExpand.cpp W65816TiedDefSpill.cpp W65816ABridgeViaX.cpp diff --git a/src/llvm/lib/Target/W65816/W65816.h b/src/llvm/lib/Target/W65816/W65816.h index e3b4efa..3a84bf1 100644 --- a/src/llvm/lib/Target/W65816/W65816.h +++ b/src/llvm/lib/Target/W65816/W65816.h @@ -66,6 +66,12 @@ FunctionPass *createW65816StackSlotCleanup(); // that cancel. See W65816SepRepCleanup.cpp. FunctionPass *createW65816SepRepCleanup(); +// Pre-emit pass (last): deletes far-pointer re-staging into DP $E0..$E3 +// when the scratch already holds the same pointer, plus the loads, +// transfers and stack temporaries that only fed it. See +// W65816FarPtrStageReuse.cpp. +FunctionPass *createW65816FarPtrStageReuse(); + // Pre-emit pass: expands long conditional branches into the // `INVERTED_Bxx skip ; BRA target ; skip:` pattern when the byte // distance to the target exceeds the +/-128 reach of an 8-bit-PCREL @@ -195,6 +201,7 @@ void initializeW65816DAGToDAGISelLegacyPass(PassRegistry &); void initializeW65816StackSlotCleanupPass(PassRegistry &); void initializeW65816I32IncFoldPass(PassRegistry &); void initializeW65816SepRepCleanupPass(PassRegistry &); +void initializeW65816FarPtrStageReusePass(PassRegistry &); void initializeW65816BranchExpandPass(PassRegistry &); void initializeW65816TiedDefSpillPass(PassRegistry &); void initializeW65816ABridgeViaXPass(PassRegistry &); diff --git a/src/llvm/lib/Target/W65816/W65816FarPtrStageReuse.cpp b/src/llvm/lib/Target/W65816/W65816FarPtrStageReuse.cpp new file mode 100644 index 0000000..94b566b --- /dev/null +++ b/src/llvm/lib/Target/W65816/W65816FarPtrStageReuse.cpp @@ -0,0 +1,1237 @@ +//===-- W65816FarPtrStageReuse.cpp - Drop redundant $E0..$E3 staging ------===// +// +// Pre-emit pass (runs last). Every far-pointer access built by the +// LDAptr32 / STAptr32 / STBptr32 inserters (W65816ISelLowering.cpp) stages +// the pointer into DP $E0 (low word) and $E2 (bank word) and then goes +// through [$E0],Y. The inserter stages before EVERY access: its own +// staging CSE is pre-RA and per basic block, so it cannot see through the +// little diamonds an unrolled loop is full of, and it has to treat every +// frame-index ALU pseudo as clobbering $E2 because it cannot know yet +// whether the frame will be far (FrameLowering decides that in the +// prologue). After RA the staging also routes through X and a fresh stack +// temporary: +// +// LDA_DP lo ; TAX ; LDA_DP bank ; STA_StackRel t ; TXA ; STA_DP $E0 ; +// LDA_StackRel t ; STA_DP $E2 +// +// Once the frame is final every instruction is concrete, so this pass can +// track what $E0/$E2 hold across the whole function and: +// 1. delete stores that write the value already there (the whole point), +// 2. delete loads/transfers that re-load a value the register already +// holds, when N/Z are not read before being set again, +// 3. delete loads/transfers whose result is dead (same N/Z condition), +// 4. delete stack-temporary stores that nothing in the function reads -- +// only when the stack pointer is tracked everywhere and no stack +// address escapes (TSC/TSX used for anything but SP arithmetic). +// Rounds repeat until nothing changes, since each step exposes the next +// (a deleted staging leaves the value spill/reload around it adjacent). +// +// Measured on the C version of Space Taxi's sprite marshal (83 far +// accesses through one struct pointer): 4183 -> 1603 cycles per call. +// Regression test: test/CodeGen/W65816/far-ptr-stage-reuse.mir. +// +// Soundness notes: +// * Values are symbols. A symbol created at an instruction names the +// content it loaded; facts are intersected at control-flow joins, and +// every cycle through that instruction has an entry edge on which the +// symbol does not exist yet, so a symbol never survives into its own +// next execution. +// * C objects never live in the direct page, so a store through a +// pointer ([dp],Y, (dp),Y, (sr,S),Y, absolute, long) does not touch DP +// facts. It does forget stack-slot facts. +// * Direct-page-indexed stores forget every DP fact; calls, inline asm, +// TCD/PLD, block moves and anything unmodelled forget everything. +// * STA_DP carries no implicit use of A in this backend's MIR and the +// block live-in lists are stale by now, so register effects and +// liveness come from this file's own per-opcode model. +// +// Kill switch: W65816_DISABLE_FARPTR_REUSE=1. +// +//===----------------------------------------------------------------------===// + +#include "W65816.h" +#include "W65816InstrInfo.h" +#include "W65816Subtarget.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/PostOrderIterator.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/STLFunctionalExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/CodeGen/MachineFunctionPass.h" +#include "llvm/CodeGen/MachineInstrBuilder.h" +#include "llvm/Support/Debug.h" + +#include + +using namespace llvm; + +#define DEBUG_TYPE "w65816-farptr-reuse" + +namespace { + +// Symbolic value id; 0 means unknown. +using SymId = unsigned; + +// Register width from the M / X status bits. +enum Width : int8_t { WUnknown = -1, W8 = 0, W16 = 1 }; + +// The scratch words the far-pointer staging writes. +constexpr unsigned StageLo = 0xE0; +constexpr unsigned StageBank = 0xE2; + +// Liveness bit set: 1 A, 2 X, 4 Y, 8 N/Z. +constexpr unsigned LiveAll = 15; + +// RTL / RTS: a return whose implicit uses are exactly what the caller reads. +static bool isPlainReturn(const MachineInstr &MI) { + return MI.getOpcode() == W65816::RTL || MI.getOpcode() == W65816::RTS; +} + +// Dataflow state carried across blocks: the three registers, DP word facts +// and the M / X widths. Stack-slot facts stay inside a block. +struct FlowState { + bool Top = true; // not reached yet + SymId A = 0, X = 0, Y = 0; + std::map DP; + Width M = WUnknown, XW = WUnknown; + + bool operator==(const FlowState &O) const { + return Top == O.Top && A == O.A && X == O.X && Y == O.Y && DP == O.DP && + M == O.M && XW == O.XW; + } + bool operator!=(const FlowState &O) const { return !(*this == O); } +}; + +// A stack push remembered inside a block, so PLA / PLX / PLY / PLP can +// restore what PHA / PHX / PHY / PHP saved. +struct Push { + char Kind; // 'A', 'X', 'Y', 'P' or '?' + SymId Sym; + Width W, XWSaved; + int Bytes; +}; + +// Per-block walking context. +struct BlockCtx { + std::map SR; // word facts keyed by normalised offset + bool SPKnown = true; + int SP = 0; // bytes pushed since the block began + SmallVector Pushes; + bool ASP = false; // A holds SP-derived value: base - ASPDelta + int ASPDelta = 0; + int8_t Carry = -1; // -1 unknown, 0 clear, 1 set +}; + +// Register / flag effects of one instruction, for liveness. +struct Effects { + bool UseA = false, UseX = false, UseY = false, UseNZ = false; + bool DefA = false, DefX = false, DefY = false, DefNZ = false; +}; + +class W65816FarPtrStageReuse : public MachineFunctionPass { +public: + static char ID; + W65816FarPtrStageReuse() : MachineFunctionPass(ID) {} + bool runOnMachineFunction(MachineFunction &MF) override; + StringRef getPassName() const override { + return "W65816 far-pointer staging reuse"; + } + void getAnalysisUsage(AnalysisUsage &AU) const override { + MachineFunctionPass::getAnalysisUsage(AU); + } + +private: + const W65816InstrInfo *TII = nullptr; + MachineFunction *MF = nullptr; + + // Symbol allocation, deterministic per (instruction, slot) so the + // fixed-point iteration converges. + DenseMap, SymId> FreshSyms; + DenseMap ConstSyms; + SymId NextSym = 1; + + // Results of the final marking walk. + SmallPtrSet RedundantStores; + SmallPtrSet RedundantLoads; + // M and X widths in force at each instruction (from the marking walk). + DenseMap> ModeAt; + Width modeM(const MachineInstr &MI) const { + auto It = ModeAt.find(&MI); + return It != ModeAt.end() ? It->second.first : WUnknown; + } + Width modeX(const MachineInstr &MI) const { + auto It = ModeAt.find(&MI); + return It != ModeAt.end() ? It->second.second : WUnknown; + } + + SymId fresh(const MachineInstr &MI, unsigned Slot); + SymId constant(unsigned V); + StringRef name(const MachineInstr &MI) const { + return TII->getName(MI.getOpcode()); + } + + bool runRound(); + void analyse(); + void transfer(MachineInstr &MI, FlowState &S, BlockCtx &C, bool Mark); + Effects effects(const MachineInstr &MI, Width M) const; + // Registers A, X, Y and the N/Z flags live out of each block, from a + // backward dataflow over this file's own effects model. + DenseMap LiveOut; + static unsigned uses(const Effects &E) { + return (E.UseA ? 1u : 0u) | (E.UseX ? 2u : 0u) | (E.UseY ? 4u : 0u) | + (E.UseNZ ? 8u : 0u); + } + static unsigned defs(const Effects &E) { + return (E.DefA ? 1u : 0u) | (E.DefX ? 2u : 0u) | (E.DefY ? 4u : 0u) | + (E.DefNZ ? 8u : 0u); + } + void computeLiveness(); + bool deleteDead(); + bool deleteDeadStackStores(); +}; + +} // namespace + +char W65816FarPtrStageReuse::ID = 0; + +INITIALIZE_PASS(W65816FarPtrStageReuse, DEBUG_TYPE, + "W65816 far-pointer staging reuse", false, false) + +// --------------------------------------------------------------------------- +// Opcode classification. The backend's opcode names are systematic: +// _ for real instructions, lower-case suffixes for the few +// pseudos that survive to pre-emit. + +static bool endsWithAny(StringRef N, std::initializer_list L) { + for (StringRef S : L) + if (N.ends_with(S)) + return true; + return false; +} + +static bool startsWithAny(StringRef N, std::initializer_list L) { + for (StringRef S : L) + if (N.starts_with(S)) + return true; + return false; +} + +// Direct-page address or stack-relative offset operand. +static bool firstImm(const MachineInstr &MI, int64_t &V) { + for (const MachineOperand &MO : MI.operands()) { + if (MO.isImm()) { + V = MO.getImm(); + return true; + } + } + return false; +} + +static bool isDPDirect(StringRef N) { + return N.ends_with("_DP") && !N.starts_with("PEI"); +} +static bool isDPIndexed(StringRef N) { return endsWithAny(N, {"_DPX", "_DPY"}); } +static bool isStackRel(StringRef N) { return N.ends_with("_StackRel"); } + +// Stores and read-modify-writes, by name. "STP" (stop the CPU) is not one. +static bool isStoreName(StringRef N) { + if (N == "STP") + return false; + return startsWithAny(N, {"STA", "STX", "STY", "STZ", "STB"}); +} +static bool isRMWName(StringRef N) { + if (endsWithAny(N, {"_A"})) + return false; + return startsWithAny(N, {"INC_", "DEC_", "ASL_", "LSR_", "ROL_", "ROR_", + "TSB_", "TRB_", "INC_HI_IF_CARRY"}); +} + +static bool isNZBranch(StringRef N) { + return N == "BEQ" || N == "BNE" || N == "BMI" || N == "BPL"; +} + +// Loads, ALU operations and compares only ever read memory. Several of +// them (ADC_Imm16, SBC_Imm16, LDA_DP, ...) carry TableGen's default +// mayStore, which must not make the model forget every DP fact: a pointer +// bump (CLC ; ADC #n) would then end all reuse in the loop around it. +static bool isReadOnlyName(StringRef N) { + return startsWithAny(N, {"LDA", "LDX", "LDY", "ADC", "SBC", "AND", "ORA", + "EOR", "CMP", "CPX", "CPY", "BIT"}); +} + +// Pointer, absolute and long addressing: the target is never the DP page. +static bool isThroughPointer(StringRef N) { + return N.contains("_DPInd") || N.contains("StackRelIndY") || + N.contains("_Abs") || N.contains("_Long") || N.contains("abs") || + N.contains("long") || N.contains("ptr"); +} + +// Instruction families the value model understands. Anything else makes +// the model forget everything. +static bool isKnownFamily(StringRef N) { + return isStoreName(N) || isRMWName(N) || isNZBranch(N) || + startsWithAny(N, {"LDA", "LDX", "LDY", "ADC", "SBC", "AND", "ORA", + "EOR", "CMP", "CPX", "CPY", "BIT", "ASL_A", "LSR_A", + "ROL_A", "ROR_A", "ASLA", "LSRA", "ASRA", "INA", + "DEA", "INX", "DEX", "INY", "DEY", "XBA", "NEG", + "SHL", "SRL", "SRA", "BRA", "BRL", "BCC", "BCS", + "BVC", "BVS", "JMP_Abs", "JML_Long", "NOP", "CLI", + "SEI", "CLD", "SED", "CLV", "MVN", "MVP"}); +} + +static bool changesA(StringRef N) { + return startsWithAny(N, {"LDA", "ADC", "SBC", "AND", "ORA", "EOR", "ASL_A", + "LSR_A", "ROL_A", "ROR_A", "ASLA", "LSRA", "ASRA", + "INA", "DEA", "XBA", "NEG", "SHL", "SRL", "SRA", + "MVN", "MVP"}); +} +static bool changesX(StringRef N) { + return startsWithAny(N, {"LDX", "INX", "DEX", "MVN", "MVP"}); +} +static bool changesY(StringRef N) { + return startsWithAny(N, {"LDY", "INY", "DEY", "MVN", "MVP"}); +} +static bool changesCarry(StringRef N) { + return startsWithAny(N, {"ADC", "SBC", "ASL", "LSR", "ROL", "ROR", "CMP", + "CPX", "CPY", "ASLA", "LSRA", "ASRA", "NEG", "SHL", + "SRL", "SRA", "INC_HI_IF_CARRY"}); +} + +Effects W65816FarPtrStageReuse::effects(const MachineInstr &MI, + Width M) const { + Effects E; + StringRef N = name(MI); + if (MI.isCall()) { + E.UseA = E.UseX = E.UseY = true; + E.DefA = E.DefX = E.DefY = E.DefNZ = true; + return E; + } + if (MI.isReturn()) { + if (isPlainReturn(MI)) { + // The return-value registers are its implicit uses (A, A:X, A:X:Y); + // the caller reads nothing else, flags included. + E.UseA = MI.readsRegister(W65816::A, nullptr); + E.UseX = MI.readsRegister(W65816::X, nullptr); + E.UseY = MI.readsRegister(W65816::Y, nullptr); + return E; + } + E.UseA = E.UseX = E.UseY = E.UseNZ = true; + return E; + } + // Operand-level uses always count; operand defs never make a register + // dead (they may be partial), except through the tables below. + E.UseA |= MI.readsRegister(W65816::A, nullptr); + E.UseX |= MI.readsRegister(W65816::X, nullptr); + E.UseY |= MI.readsRegister(W65816::Y, nullptr); + + bool Known = true; + if (N == "ADJCALLSTACKDOWN" || N == "ADJCALLSTACKUP") + return E; // left in place only when there is nothing to pop: a no-op + // Full definitions (these do not read the register). + if (N == "ADDframe" || N == "ALLOCAfi") { + Known = false; + } else if (startsWithAny(N, {"LDA_", "LDAi16imm", "LDAi8imm", "LDAabs", "LDA8", + "LDAptr", "PLA", "TXA", "TYA", "TDC", "TSC"})) { + // An 8-bit load or transfer keeps A's high byte: not a full def. + if (M == W16 && !N.starts_with("LDA8") && !N.starts_with("LDAi8imm")) + E.DefA = true; + else + E.UseA = true; + } else if (startsWithAny(N, {"LDX", "PLX", "TAX", "TYX", "TSX"})) { + E.DefX = true; + } else if (startsWithAny(N, {"LDY", "PLY", "TAY", "TXY"})) { + E.DefY = true; + } else if (startsWithAny(N, {"ADC", "SBC", "AND", "ORA", "EOR", "ASL_A", + "LSR_A", "ROL_A", "ROR_A", "ASLA", "LSRA", + "ASRA", "INA", "DEA", "XBA", "NEG", "SHL", + "SRL", "SRA", "ADD", "SUB"})) { + E.UseA = true; + } else if (N == "PUSH16X") { + E.UseX = true; + } else if (startsWithAny(N, {"CMP", "BIT", "TSB", "TRB", "PHA", "TCD", + "TCS", "PUSH16"})) { + E.UseA = true; + } else if (startsWithAny(N, {"INX", "DEX", "CPX", "PHX", "TXS"})) { + E.UseX = true; + } else if (startsWithAny(N, {"INY", "DEY", "CPY", "PHY"})) { + E.UseY = true; + } else if (isStoreName(N) || isRMWName(N) || isNZBranch(N) || + startsWithAny(N, {"PHP", "PLP", "PHB", "PHD", "PHK", "PLB", "PLD", + "PEA", "PEI", "PER", "REP", "SEP", "CLC", "SEC", + "CLI", "SEI", "CLD", "SED", "CLV", "NOP", "BRA", + "BRL", "BCC", "BCS", "BVC", "BVS", "JMP", "JML"})) { + // Handled below or no register effect. + } else { + Known = false; + } + // Transfers read their source. + if (startsWithAny(N, {"TAX", "TAY", "TCD", "TCS"})) + E.UseA = true; + if (startsWithAny(N, {"TXA", "TXY", "TXS"})) + E.UseX = true; + if (startsWithAny(N, {"TYA", "TYX"})) + E.UseY = true; + // Stores read their register. + if (N.starts_with("STA") || N.starts_with("STB")) + E.UseA = true; + if (N.starts_with("STX")) + E.UseX = true; + if (N.starts_with("STY")) + E.UseY = true; + // Indexed addressing reads the index register. + if (endsWithAny(N, {"_AbsX", "_DPX", "_LongX", "_DPIndX", "absX", "AbsIndX"})) + E.UseX = true; + if (endsWithAny(N, {"_AbsY", "_DPIndY", "_DPIndLongY", "_StackRelIndY", + "_DPY", "_indY"})) + E.UseY = true; + if (N == "MVN" || N == "MVP") { + E.UseA = E.UseX = E.UseY = true; + E.DefA = E.DefX = E.DefY = false; + } + // N/Z. + if (isNZBranch(N) || N == "PHP") + E.UseNZ = true; + if (N.starts_with("SELECT")) + E.UseNZ = true; + bool SetsNZ = + startsWithAny(N, {"LDA", "LDX", "LDY", "PLA", "PLX", "PLY", "PLP", "TAX", + "TAY", "TXA", "TYA", "TXY", "TYX", "TSC", "TDC", "TSX", + "TCD", "ADC", "SBC", "AND", "ORA", "EOR", "CMP", "CPX", + "CPY", "ASL", "LSR", "ROL", "ROR", "INC", "DEC", "INA", + "DEA", "INX", "DEX", "INY", "DEY", "XBA", "NEG", "SHL", + "SRL", "SRA", "ADD", "SUB"}) || + (N.starts_with("BIT") && N != "BIT_Imm16" && N != "BIT_Imm8"); + if (N.starts_with("INC_HI_IF_CARRY")) + SetsNZ = false; // conditional: may leave the flags alone + if (SetsNZ) + E.DefNZ = true; + if (!Known) { + // Unmodelled: assume it reads everything and defines nothing. + E.UseA = E.UseX = E.UseY = E.UseNZ = true; + E.DefA = E.DefX = E.DefY = E.DefNZ = false; + } + return E; +} + +// --------------------------------------------------------------------------- + +SymId W65816FarPtrStageReuse::fresh(const MachineInstr &MI, unsigned Slot) { + auto Key = std::make_pair(&MI, Slot); + auto It = FreshSyms.find(Key); + if (It != FreshSyms.end()) + return It->second; + SymId S = NextSym++; + FreshSyms[Key] = S; + return S; +} + +SymId W65816FarPtrStageReuse::constant(unsigned V) { + V &= 0xFFFF; + auto It = ConstSyms.find(V); + if (It != ConstSyms.end()) + return It->second; + SymId S = NextSym++; + ConstSyms[V] = S; + return S; +} + +static void killDPWord(FlowState &S, int64_t D) { + S.DP.erase((unsigned)(D - 1)); + S.DP.erase((unsigned)D); + S.DP.erase((unsigned)(D + 1)); +} + +static void killSRWord(BlockCtx &C, int K) { + C.SR.erase(K - 1); + C.SR.erase(K); + C.SR.erase(K + 1); +} + +// Forget everything a call, inline asm or unmodelled side effect could +// have changed. +static void forgetAll(FlowState &S, BlockCtx &C) { + S.A = S.X = S.Y = 0; + S.DP.clear(); + C.SR.clear(); + C.ASP = false; + C.Carry = -1; +} + +void W65816FarPtrStageReuse::transfer(MachineInstr &MI, FlowState &S, + BlockCtx &C, bool Mark) { + if (MI.isMetaInstruction() || MI.isDebugInstr()) + return; + if (Mark) + ModeAt[&MI] = {S.M, S.XW}; + StringRef N = name(MI); + int64_t Imm = 0; + bool HasImm = firstImm(MI, Imm); + + if (MI.isInlineAsm()) { + forgetAll(S, C); + S.M = S.XW = WUnknown; + C.SPKnown = false; + C.Pushes.clear(); + return; + } + if (MI.isCall()) { + forgetAll(S, C); + S.M = S.XW = W16; // the w65816 ABI returns with M = X = 16 + return; + } + + // Register widths. + if (N == "REP" || N == "SEP") { + bool Set = N == "SEP"; + if (HasImm) { + if (Imm & 0x20) + S.M = Set ? W8 : W16; + if (Imm & 0x10) { + S.XW = Set ? W8 : W16; + if (Set) + S.X = S.Y = 0; // X = 8 zeroes the index high bytes + } + if (Imm & 0x01) + C.Carry = Set ? 1 : 0; + } else { + S.M = S.XW = WUnknown; + C.Carry = -1; + } + return; + } + if (N == "CLC") { + C.Carry = 0; + return; + } + if (N == "SEC") { + C.Carry = 1; + return; + } + + // Stack pushes and pulls. + auto push = [&](char Kind, SymId Sym, int Bytes) { + if (Bytes <= 0) { + C.SPKnown = false; + C.Pushes.clear(); + return; + } + C.Pushes.push_back({Kind, Sym, S.M, S.XW, Bytes}); + C.SP += Bytes; + }; + auto pull = [&](char Kind, int Bytes, SymId &Out) { + Out = 0; + if (Bytes <= 0) { + C.SPKnown = false; + C.Pushes.clear(); + return; + } + if (!C.Pushes.empty()) { + Push P = C.Pushes.pop_back_val(); + if (P.Kind == Kind && P.Bytes == Bytes) + Out = P.Sym; + } + C.SP -= Bytes; + }; + auto widthBytes = [](Width W) { return W == W16 ? 2 : W == W8 ? 1 : 0; }; + if (N == "PHP") { + C.Pushes.push_back({'P', 0, S.M, S.XW, 1}); + C.SP += 1; + return; + } + if (N == "PLP") { + if (!C.Pushes.empty() && C.Pushes.back().Kind == 'P') { + S.M = C.Pushes.back().W; + S.XW = C.Pushes.back().XWSaved; + C.Pushes.pop_back(); + } else { + S.M = S.XW = WUnknown; + C.Pushes.clear(); + } + C.SP -= 1; + C.Carry = -1; + return; + } + if (N == "PHA") { + push('A', S.M == W16 ? S.A : 0, widthBytes(S.M)); + return; + } + if (N == "PHX") { + push('X', S.X, widthBytes(S.XW)); + return; + } + if (N == "PHY") { + push('Y', S.Y, widthBytes(S.XW)); + return; + } + if (N == "PHB" || N == "PHK") { + push('?', 0, 1); + return; + } + if (N == "PHD" || N == "PEA" || N == "PEI_DP" || N == "PER" || + N == "PUSH16" || N == "PUSH16X") { + push('?', 0, 2); + return; + } + if (N == "PLA") { + SymId V; + pull('A', widthBytes(S.M), V); + S.A = S.M == W16 ? V : 0; + C.ASP = false; + C.Carry = -1; + return; + } + if (N == "PLX" || N == "PLY") { + SymId V; + pull(N == "PLX" ? 'X' : 'Y', widthBytes(S.XW), V); + (N == "PLX" ? S.X : S.Y) = V; + return; + } + if (N == "PLB") { + SymId V; + pull('?', 1, V); + return; + } + if (N == "PLD") { + SymId V; + pull('?', 2, V); + S.DP.clear(); // the direct page moved + return; + } + + // Stack pointer arithmetic: TSC ; CLC ; ADC #k ; TCS and friends. + if (N == "TSC") { + S.A = 0; + C.ASP = C.SPKnown; + C.ASPDelta = C.SP; + return; + } + if ((N == "ADC_Imm16" && C.Carry == 0) || N == "ADCi16imm") { + if (C.ASP && HasImm) { + C.ASPDelta -= (int)Imm; + S.A = 0; + C.Carry = -1; + return; + } + } + if ((N == "SBC_Imm16" && C.Carry == 1) || N == "SBCi16imm") { + if (C.ASP && HasImm) { + C.ASPDelta += (int)Imm; + S.A = 0; + C.Carry = -1; + return; + } + } + if (N == "TCS") { + if (C.ASP) { + C.SP = C.ASPDelta; // same base, so normalised SR keys stay valid + } else { + C.SPKnown = false; + C.SR.clear(); + C.Pushes.clear(); + } + return; + } + if (N == "TXS") { + C.SPKnown = false; + C.SR.clear(); + C.Pushes.clear(); + return; + } + if (N == "TCD") { + S.DP.clear(); + return; + } + + // Loads and transfers that the value tracking follows. + auto setA = [&](SymId V) { + S.A = V; + C.ASP = false; + }; + if (N == "LDA_DP" && HasImm) { + if (S.M == W16) { + auto It = S.DP.find((unsigned)Imm); + SymId V = It != S.DP.end() ? It->second : 0; + if (!V) { + V = fresh(MI, 0); + S.DP[(unsigned)Imm] = V; + } + if (Mark && S.A == V) + RedundantLoads.insert(&MI); + setA(V); + } else { + setA(0); + } + return; + } + if (N == "LDA_StackRel" && HasImm) { + if (S.M == W16 && C.SPKnown) { + int K = (int)Imm - C.SP; + auto It = C.SR.find(K); + SymId V = It != C.SR.end() ? It->second : 0; + if (!V) { + V = fresh(MI, 1); + C.SR[K] = V; + } + if (Mark && S.A == V) + RedundantLoads.insert(&MI); + setA(V); + } else { + setA(0); + } + return; + } + if (N == "LDA_Imm16" || N == "LDAi16imm") { + int64_t V = 0; + bool Got = false; + for (const MachineOperand &MO : MI.operands()) + if (MO.isImm()) { + V = MO.getImm(); + Got = true; + break; + } + SymId Sym = (S.M == W16 && Got) ? constant((unsigned)V) : 0; + if (Mark && Sym && S.A == Sym) + RedundantLoads.insert(&MI); + setA(Sym); + return; + } + if ((N == "LDX_DP" || N == "LDY_DP") && HasImm) { + SymId &R = N == "LDX_DP" ? S.X : S.Y; + if (S.XW == W16) { + auto It = S.DP.find((unsigned)Imm); + SymId V = It != S.DP.end() ? It->second : 0; + if (!V) { + V = fresh(MI, 0); + S.DP[(unsigned)Imm] = V; + } + if (Mark && R == V) + RedundantLoads.insert(&MI); + R = V; + } else { + R = 0; + } + return; + } + if (N == "LDX_Imm16" || N == "LDXi16imm" || N == "LDY_Imm16") { + SymId &R = N == "LDY_Imm16" ? S.Y : S.X; + SymId Sym = (S.XW == W16 && HasImm) ? constant((unsigned)Imm) : 0; + if (Mark && Sym && R == Sym) + RedundantLoads.insert(&MI); + R = Sym; + return; + } + auto copyReg = [&](SymId &Dst, SymId Src, bool Ok) { + SymId V = Ok ? Src : 0; + if (Mark && V && Dst == V) + RedundantLoads.insert(&MI); + Dst = V; + }; + if (N == "TAX") { + copyReg(S.X, S.A, S.XW == W16); + return; + } + if (N == "TAY") { + copyReg(S.Y, S.A, S.XW == W16); + return; + } + if (N == "TXA") { + copyReg(S.A, S.X, S.M == W16 && S.XW == W16); + C.ASP = false; + return; + } + if (N == "TYA") { + copyReg(S.A, S.Y, S.M == W16 && S.XW == W16); + C.ASP = false; + return; + } + if (N == "TXY") { + copyReg(S.Y, S.X, S.XW == W16); + return; + } + if (N == "TYX") { + copyReg(S.X, S.Y, S.XW == W16); + return; + } + + // Direct-page stores: the staging stores are recognised here. + if ((N == "STA_DP" || N == "STZ_DP" || N == "STX_DP" || N == "STY_DP") && + HasImm) { + Width W = N == "STA_DP" || N == "STZ_DP" ? S.M : S.XW; + SymId *Src = N == "STA_DP" ? &S.A + : N == "STX_DP" ? &S.X + : N == "STY_DP" ? &S.Y + : nullptr; + // An unknown register value gets a name here: the store and every + // later copy of it then compare equal. + if (W == W16 && Src && !*Src) + *Src = fresh(MI, 2); + SymId V = Src ? *Src : constant(0); + if (W == W16) { + auto It = S.DP.find((unsigned)Imm); + if (V && It != S.DP.end() && It->second == V) { + // Writes what is already there. Only the staging words are + // deleted: they are compiler scratch, so nothing else observes the + // store (the rest of the DP page is left to the passes that own it). + if (Mark && (Imm == StageLo || Imm == StageBank)) + RedundantStores.insert(&MI); + return; + } + killDPWord(S, Imm); + if (V) + S.DP[(unsigned)Imm] = V; + } else { + killDPWord(S, Imm); + } + return; + } + if (N == "STA_StackRel" && HasImm) { + if (!C.SPKnown) { + C.SR.clear(); + return; + } + int K = (int)Imm - C.SP; + if (S.M == W16) { + if (!S.A) + S.A = fresh(MI, 2); // name the unknown value, as for DP stores + auto It = C.SR.find(K); + if (S.A && It != C.SR.end() && It->second == S.A) { + if (Mark) + RedundantStores.insert(&MI); + return; + } + killSRWord(C, K); + if (S.A) + C.SR[K] = S.A; + } else { + killSRWord(C, K); + } + return; + } + + if (N == "ADJCALLSTACKDOWN" || N == "ADJCALLSTACKUP") + return; // no-ops once PEI has left them in place + if (N == "ADDframe" || N == "ALLOCAfi") { + // TSC-based frame address (ALLOCAfi also moves S): forget the stack. + setA(0); + C.SR.clear(); + if (N == "ALLOCAfi") { + C.SPKnown = false; + C.Pushes.clear(); + } + C.Carry = -1; + return; + } + + // Everything else. Anything outside the families this file knows + // forgets everything; the rest loses exactly what it can change. + if (!isKnownFamily(N)) { + forgetAll(S, C); + C.SPKnown = false; + C.Pushes.clear(); + S.M = S.XW = WUnknown; + return; + } + if (changesA(N) || MI.modifiesRegister(W65816::A, nullptr)) + setA(0); + if (changesX(N) || MI.modifiesRegister(W65816::X, nullptr)) + S.X = 0; + if (changesY(N) || MI.modifiesRegister(W65816::Y, nullptr)) + S.Y = 0; + if (changesCarry(N)) + C.Carry = -1; + + if (N == "MVN" || N == "MVP") { + S.DP.clear(); + C.SR.clear(); + return; + } + if (isStoreName(N) || isRMWName(N) || + (MI.mayStore() && !isReadOnlyName(N))) { + if (isDPDirect(N) && HasImm) { + killDPWord(S, Imm); + } else if (isDPIndexed(N)) { + S.DP.clear(); + } else if (isStackRel(N) && HasImm && C.SPKnown) { + killSRWord(C, (int)Imm - C.SP); + } else if (isThroughPointer(N)) { + // Through a pointer or an absolute address: never the DP page. + C.SR.clear(); + } else { + S.DP.clear(); + C.SR.clear(); + } + } +} + +// --------------------------------------------------------------------------- + +static FlowState meet(const FlowState &A, const FlowState &B) { + if (A.Top) + return B; + if (B.Top) + return A; + FlowState R; + R.Top = false; + R.A = A.A == B.A ? A.A : 0; + R.X = A.X == B.X ? A.X : 0; + R.Y = A.Y == B.Y ? A.Y : 0; + R.M = A.M == B.M ? A.M : WUnknown; + R.XW = A.XW == B.XW ? A.XW : WUnknown; + for (const auto &KV : A.DP) { + auto It = B.DP.find(KV.first); + if (It != B.DP.end() && It->second == KV.second) + R.DP.insert(KV); + } + return R; +} + +void W65816FarPtrStageReuse::analyse() { + RedundantStores.clear(); + RedundantLoads.clear(); + ModeAt.clear(); + + DenseMap In, Out; + ReversePostOrderTraversal RPOT(MF); + MachineBasicBlock *Entry = &MF->front(); + + bool Changed = true; + unsigned Iter = 0; + while (Changed && Iter++ < 64) { + Changed = false; + for (MachineBasicBlock *MBB : RPOT) { + FlowState S; + if (MBB == Entry) { + S.Top = false; // nothing known on entry, widths included + } else { + for (MachineBasicBlock *P : MBB->predecessors()) + S = meet(S, Out.lookup(P)); + if (S.Top) + continue; // no predecessor reached yet + } + In[MBB] = S; + BlockCtx C; + for (MachineInstr &MI : *MBB) + transfer(MI, S, C, false); + if (Out.lookup(MBB) != S) { + Out[MBB] = S; + Changed = true; + } + } + } + if (Changed) { + // Did not converge: mark nothing. + LLVM_DEBUG(dbgs() << "farptr-reuse: no fixed point in " << MF->getName() + << "\n"); + return; + } + for (MachineBasicBlock &MBB : *MF) { + auto It = In.find(&MBB); + if (It == In.end()) + continue; // unreachable + FlowState S = It->second; + BlockCtx C; + for (MachineInstr &MI : MBB) + transfer(MI, S, C, true); + } +} + +void W65816FarPtrStageReuse::computeLiveness() { + DenseMap LiveIn; + LiveOut.clear(); + bool Changed = true; + while (Changed) { + Changed = false; + for (MachineBasicBlock *MBB : post_order(MF)) { + unsigned Out = 0; + if (MBB->succ_empty()) { + // A plain return's own uses say what the caller reads; any other + // way out (RTI, a jump out, a noreturn call) may read everything. + auto Last = MBB->getLastNonDebugInstr(); + if (Last == MBB->end() || !isPlainReturn(*Last)) + Out = LiveAll; + } + for (MachineBasicBlock *Succ : MBB->successors()) + Out |= LiveIn.lookup(Succ); + unsigned Live = Out; + for (auto It = MBB->rbegin(); It != MBB->rend(); ++It) { + if (It->isMetaInstruction() || It->isDebugInstr()) + continue; + Effects E = effects(*It, modeM(*It)); + Live = (Live & ~defs(E)) | uses(E); + } + auto Seen = LiveIn.find(MBB); + if (Seen == LiveIn.end() || Seen->second != Live || + LiveOut[MBB] != Out) { + LiveOut[MBB] = Out; + LiveIn[MBB] = Live; + Changed = true; + } + } + } +} + +// The destination register of the loads and transfers this pass may +// delete: 0 A, 1 X, 2 Y, or -1 for anything else. +static int pureDest(StringRef N) { + if (N == "LDA_DP" || N == "LDA_StackRel" || N == "LDA_Imm16" || + N == "LDAi16imm" || N == "TXA" || N == "TYA") + return 0; + if (N == "LDX_DP" || N == "LDX_Imm16" || N == "LDXi16imm" || N == "TAX" || + N == "TYX") + return 1; + if (N == "LDY_DP" || N == "LDY_Imm16" || N == "TAY" || N == "TXY") + return 2; + return -1; +} + +// Delete in three phases, each against liveness that is exact for the code +// as it then stands: +// 1. stores that write what is already there (removes uses only); +// 2. loads / transfers of a value the register already holds, when N/Z +// are dead after them. This EXTENDS the life of the earlier +// definition, so no dead-code decision may be taken in the same phase +// (it would see stale liveness: the earlier definition looked dead +// because this load used to redefine the register); +// 3. with liveness recomputed, loads / transfers whose result is dead, +// walking each block backwards. Deleting dead code only shrinks +// liveness, so the phase's own deletions keep it conservative. +bool W65816FarPtrStageReuse::deleteDead() { + bool Changed = false; + for (MachineInstr *MI : RedundantStores) { + MI->eraseFromParent(); + Changed = true; + } + RedundantStores.clear(); + + auto walk = [&](function_ref Kill) { + computeLiveness(); + SmallVector Doomed; + for (MachineBasicBlock &MBB : *MF) { + // Blocks the liveness walk never reached keep everything. + auto Found = LiveOut.find(&MBB); + unsigned Out = Found != LiveOut.end() ? Found->second : LiveAll; + bool Live[4] = {(Out & 1) != 0, (Out & 2) != 0, (Out & 4) != 0, + (Out & 8) != 0}; + for (auto It = MBB.rbegin(); It != MBB.rend(); ++It) { + MachineInstr &MI = *It; + if (MI.isMetaInstruction() || MI.isDebugInstr()) + continue; + if (Kill(MI, Live)) { + Doomed.push_back(&MI); + continue; // gone: neither its uses nor its defs count + } + Effects E = effects(MI, modeM(MI)); + unsigned L = (Live[0] ? 1u : 0u) | (Live[1] ? 2u : 0u) | + (Live[2] ? 4u : 0u) | (Live[3] ? 8u : 0u); + L = (L & ~defs(E)) | uses(E); + for (unsigned W = 0; W < 4; ++W) + Live[W] = (L >> W) & 1u; + } + } + for (MachineInstr *MI : Doomed) + MI->eraseFromParent(); + return !Doomed.empty(); + }; + + // Phase 2. A deleted redundant load neither defines nor uses anything + // any more, and the register it re-loaded stays live across its slot + // because whatever read it still does -- exactly what "continue" models. + if (walk([&](MachineInstr &MI, const bool *Live) { + return pureDest(name(MI)) >= 0 && !Live[3] && + RedundantLoads.count(&MI); + })) + Changed = true; + RedundantLoads.clear(); + + // Phase 3. + if (walk([&](MachineInstr &MI, const bool *Live) { + int Dst = pureDest(name(MI)); + return Dst >= 0 && !Live[3] && !Live[Dst]; + })) + Changed = true; + return Changed; +} + +// Stack-temporary stores nothing reads. Needs the stack pointer tracked at +// every instruction (relative to function entry) and no stack address +// escaping; otherwise it does nothing. +bool W65816FarPtrStageReuse::deleteDeadStackStores() { + DenseMap EntrySP; + DenseMap KeyAt; // normalised offset per SR access + ReversePostOrderTraversal RPOT(MF); + EntrySP[&MF->front()] = 0; + + for (MachineBasicBlock *MBB : RPOT) { + auto It = EntrySP.find(MBB); + if (It == EntrySP.end()) + continue; // reached only by back edges; checked below + int SP = It->second; + bool ASP = false; + int ASPDelta = 0; + int8_t Carry = -1; + for (MachineInstr &MI : *MBB) { + if (MI.isMetaInstruction() || MI.isDebugInstr()) + continue; + if (MI.isInlineAsm()) + return false; + StringRef N = name(MI); + Width M = modeM(MI); + Width XW = modeX(MI); + int64_t Imm = 0; + bool HasImm = firstImm(MI, Imm); + if (N == "ADDframe" || N == "ALLOCAfi") + return false; // a stack address is formed + if (isStackRel(N) || N.ends_with("StackRelIndY")) { + if (!HasImm) + return false; + KeyAt[&MI] = (int)Imm - SP; + } + // SP-derived A may only feed SP arithmetic and TCS. + if (ASP) { + bool SPUse = N == "CLC" || N == "SEC" || N == "TCS" || + N == "ADC_Imm16" || N == "ADCi16imm" || + N == "SBC_Imm16" || N == "SBCi16imm"; + if (!SPUse) { + Effects E = effects(MI, M); + if (E.UseA) + return false; // a stack address escapes + if (E.DefA) + ASP = false; + } + } + if (N == "TSC") { + ASP = true; + ASPDelta = SP; + continue; + } + if (N == "TSX" || N == "TXS") + return false; + if (N == "CLC") { + Carry = 0; + continue; + } + if (N == "SEC") { + Carry = 1; + continue; + } + if (ASP && HasImm && + ((N == "ADC_Imm16" && Carry == 0) || N == "ADCi16imm")) { + ASPDelta -= (int)Imm; + Carry = -1; + continue; + } + if (ASP && HasImm && + ((N == "SBC_Imm16" && Carry == 1) || N == "SBCi16imm")) { + ASPDelta += (int)Imm; + Carry = -1; + continue; + } + if (N == "TCS") { + if (!ASP) + return false; + SP = ASPDelta; + ASP = false; + continue; + } + int Bytes = 0; + if (N == "PHA" || N == "PLA") + Bytes = M == W16 ? 2 : M == W8 ? 1 : -1; + else if (N == "PHX" || N == "PHY" || N == "PLX" || N == "PLY") + Bytes = XW == W16 ? 2 : XW == W8 ? 1 : -1; + else if (N == "PHP" || N == "PLP" || N == "PHB" || N == "PHK" || + N == "PLB") + Bytes = 1; + else if (N == "PHD" || N == "PLD" || N == "PEA" || N == "PEI_DP" || + N == "PER" || N == "PUSH16" || N == "PUSH16X") + Bytes = 2; + if (Bytes < 0) + return false; + if (N.starts_with("PH") || N.starts_with("PE") || N.starts_with("PUSH")) + SP += Bytes; + else if (N.starts_with("PL")) + SP -= Bytes; + if (N == "REP" || N == "SEP" || N == "PLP") + Carry = -1; + if (!N.starts_with("PH") && !N.starts_with("PL") && + effects(MI, M).DefNZ) + Carry = -1; + } + for (MachineBasicBlock *S : MBB->successors()) { + auto SIt = EntrySP.find(S); + if (SIt == EntrySP.end()) + EntrySP[S] = SP; + else if (SIt->second != SP) + return false; + } + } + // Every reachable block got an entry SP (back edges agree, checked + // above); blocks never reached are ignored. + + // Count reads per normalised byte. + DenseMap Reads; + for (auto &KV : KeyAt) { + MachineInstr *MI = KV.first; + StringRef N = name(*MI); + if (N == "STA_StackRel") + continue; + // Loads, ALU operands, (sr,S),Y pointers and anything unknown on the + // stack: all reads, two bytes to be safe. + Reads[KV.second] += 1; + Reads[KV.second + 1] += 1; + } + bool Changed = false; + for (auto &KV : KeyAt) { + MachineInstr *MI = KV.first; + if (name(*MI) != "STA_StackRel") + continue; + Width M = modeM(*MI); + if (M == WUnknown) + continue; + int Bytes = M == W16 ? 2 : 1; + bool Read = false; + for (int B = 0; B < Bytes; ++B) + if (Reads.count(KV.second + B)) + Read = true; + if (Read) + continue; + // Local frame slots normalise to offsets <= 0 (S + 1 .. S + frame + // size after the prologue's TCS); above them sit the return address + // and the incoming arguments, which the caller may read back. + if (KV.second > 0) + continue; + MI->eraseFromParent(); + Changed = true; + } + return Changed; +} + +bool W65816FarPtrStageReuse::runRound() { + analyse(); + bool Changed = deleteDead(); + if (Changed) + analyse(); // ModeAt must describe the surviving instructions + if (deleteDeadStackStores()) + Changed = true; + return Changed; +} + +bool W65816FarPtrStageReuse::runOnMachineFunction(MachineFunction &Fn) { + if (skipFunction(Fn.getFunction()) || getenv("W65816_DISABLE_FARPTR_REUSE")) + return false; + MF = &Fn; + TII = Fn.getSubtarget().getInstrInfo(); + FreshSyms.clear(); + ConstSyms.clear(); + NextSym = 1; + bool Changed = false; + for (unsigned Round = 0; Round < 6; ++Round) { + if (!runRound()) + break; + Changed = true; + } + return Changed; +} + +FunctionPass *llvm::createW65816FarPtrStageReuse() { + return new W65816FarPtrStageReuse(); +} diff --git a/src/llvm/lib/Target/W65816/W65816ISelLowering.cpp b/src/llvm/lib/Target/W65816/W65816ISelLowering.cpp index f4fd311..5f075ab 100644 --- a/src/llvm/lib/Target/W65816/W65816ISelLowering.cpp +++ b/src/llvm/lib/Target/W65816/W65816ISelLowering.cpp @@ -3018,6 +3018,22 @@ emitRROp(MachineInstr &MI, MachineBasicBlock *BB, unsigned StoreOp, return BB; } +// The `[dp],Y` access a pointer-load pseudo expands to. A volatile byte +// load gets the atomic 8-bit LDB_DPIndLongY: the usual 16-bit load + mask +// also reads address+1, a phantom read of the next register when the +// pointer is I/O. Everything else keeps the cheaper 16-bit LDA. +static bool isVolatileByteLoad(const MachineInstr &MI) { + for (const MachineMemOperand *MMO : MI.memoperands()) + if (MMO->isLoad() && MMO->isVolatile() && + MMO->getSize() == LocationSize::precise(1)) + return true; + return false; +} +static unsigned ptrLoadOpcode(const MachineInstr &MI) { + return isVolatileByteLoad(MI) ? W65816::LDB_DPIndLongY + : W65816::LDA_DPIndLongY; +} + MachineBasicBlock * W65816TargetLowering::EmitInstrWithCustomInserter(MachineInstr &MI, MachineBasicBlock *BB) const { @@ -3106,7 +3122,7 @@ W65816TargetLowering::EmitInstrWithCustomInserter(MachineInstr &MI, BuildMI(*BB, MI.getIterator(), DL, TII.get(W65816::LDY_Imm16)).addImm(0); BuildMI(*BB, MI.getIterator(), DL, - TII.get(W65816::LDA_DPIndLongY)).addImm(0xE0); + TII.get(ptrLoadOpcode(MI))).addImm(0xE0); BuildMI(*BB, MI.getIterator(), DL, TII.get(TargetOpcode::COPY), Dst).addReg(W65816::A); } else { @@ -3361,7 +3377,8 @@ W65816TargetLowering::EmitInstrWithCustomInserter(MachineInstr &MI, // a STAfi of sub_lo and an LDAfi_indY/STAfi_indY deref via the // 16-bit stack-rel-indirect-Y opcode (0xB3 / 0x93). ~4 instr per // deref saved vs the heavy [dp],Y indirect-long path. - if (DbrSafePtrs) { + // (Not for a volatile byte load: `lda (sr,s),y` is 16-bit too.) + if (DbrSafePtrs && !(IsLoad && isVolatileByteLoad(MI))) { Register PtrLo = MRI.createVirtualRegister(&W65816::Wide16RegClass); if (PeelOff) { BuildMI(*BB, MI.getIterator(), DL, TII.get(TargetOpcode::COPY), PtrLo) @@ -3602,7 +3619,7 @@ W65816TargetLowering::EmitInstrWithCustomInserter(MachineInstr &MI, BuildMI(*BB, MI.getIterator(), DL, TII.get(W65816::LDY_Imm16)).addImm(PeelOff); BuildMI(*BB, MI.getIterator(), DL, - TII.get(W65816::LDA_DPIndLongY)).addImm(0xE0); + TII.get(ptrLoadOpcode(MI))).addImm(0xE0); BuildMI(*BB, MI.getIterator(), DL, TII.get(TargetOpcode::COPY), Dst).addReg(W65816::A); } else { @@ -3694,7 +3711,7 @@ W65816TargetLowering::EmitInstrWithCustomInserter(MachineInstr &MI, BuildMI(*BB, MI.getIterator(), DL, TII.get(W65816::LDY_Imm16)).addImm(Off); BuildMI(*BB, MI.getIterator(), DL, - TII.get(W65816::LDA_DPIndLongY)).addImm(0xE0); + TII.get(ptrLoadOpcode(MI))).addImm(0xE0); BuildMI(*BB, MI.getIterator(), DL, TII.get(TargetOpcode::COPY), Dst).addReg(W65816::A); } else { @@ -3770,7 +3787,7 @@ W65816TargetLowering::EmitInstrWithCustomInserter(MachineInstr &MI, BuildMI(*BB, MI.getIterator(), DL, TII.get(W65816::LDY_Imm16)).addImm(0); BuildMI(*BB, MI.getIterator(), DL, - TII.get(W65816::LDA_DPIndLongY)).addImm(0xE0); + TII.get(ptrLoadOpcode(MI))).addImm(0xE0); BuildMI(*BB, MI.getIterator(), DL, TII.get(TargetOpcode::COPY), Dst).addReg(W65816::A); } else { @@ -3945,7 +3962,7 @@ W65816TargetLowering::EmitInstrWithCustomInserter(MachineInstr &MI, BuildMI(*BB, MI.getIterator(), DL, TII.get(W65816::LDY_Imm16)).addImm(0); BuildMI(*BB, MI.getIterator(), DL, - TII.get(W65816::LDA_DPIndLongY)).addImm(0xE0); + TII.get(ptrLoadOpcode(MI))).addImm(0xE0); BuildMI(*BB, MI.getIterator(), DL, TII.get(TargetOpcode::COPY), Dst).addReg(W65816::A); } else { diff --git a/src/llvm/lib/Target/W65816/W65816InstrInfo.cpp b/src/llvm/lib/Target/W65816/W65816InstrInfo.cpp index 0cf58c9..0390b76 100644 --- a/src/llvm/lib/Target/W65816/W65816InstrInfo.cpp +++ b/src/llvm/lib/Target/W65816/W65816InstrInfo.cpp @@ -25,9 +25,10 @@ using namespace llvm; void W65816InstrInfo::anchor() {} -// Expand STB_DPIndLongY -> SEP #$20 ; sta [dp],y ; REP #$20. Runs in the +// Expand STB_DPIndLongY -> SEP #$20 ; sta [dp],y ; REP #$20 and +// LDB_DPIndLongY -> SEP #$20 ; lda [dp],y ; REP #$20. Runs in the // post-RA pseudo-expansion pass (after the fast/greedy allocator, before -// SepRepCleanup and the AsmPrinter). Keeping the byte store as a single +// SepRepCleanup and the AsmPrinter). Keeping the byte access as a single // pseudo through register allocation is what fixes the -O0 miscompile: the // fast allocator used to wedge a 16-bit reload between a hand-emitted SEP // and REP, running it in M=8 and truncating a live value. Expanding here @@ -35,13 +36,22 @@ void W65816InstrInfo::anchor() {} // REP coalescer and the AsmPrinter's LDAi8imm mode-tracking behave exactly // as they did for the old three-instruction form. bool W65816InstrInfo::expandPostRAPseudo(MachineInstr &MI) const { - if (MI.getOpcode() != W65816::STB_DPIndLongY) + unsigned Access; + switch (MI.getOpcode()) { + case W65816::STB_DPIndLongY: + Access = W65816::STA_DPIndLongY; + break; + case W65816::LDB_DPIndLongY: + Access = W65816::LDA_DPIndLongY; + break; + default: return false; + } MachineBasicBlock &MBB = *MI.getParent(); DebugLoc DL = MI.getDebugLoc(); int64_t Addr = MI.getOperand(0).getImm(); BuildMI(MBB, MI, DL, get(W65816::SEP)).addImm(0x20); - BuildMI(MBB, MI, DL, get(W65816::STA_DPIndLongY)).addImm(Addr); + BuildMI(MBB, MI, DL, get(Access)).addImm(Addr); BuildMI(MBB, MI, DL, get(W65816::REP)).addImm(0x20); MI.eraseFromParent(); return true; diff --git a/src/llvm/lib/Target/W65816/W65816InstrInfo.td b/src/llvm/lib/Target/W65816/W65816InstrInfo.td index ab70174..630fb90 100644 --- a/src/llvm/lib/Target/W65816/W65816InstrInfo.td +++ b/src/llvm/lib/Target/W65816/W65816InstrInfo.td @@ -1200,6 +1200,18 @@ def STB_DPIndLongY : W65816Pseudo<(outs), (ins addrDP:$addr), "# STB_DPIndLongY [${addr}], y", []>; } +// Atomic i8-load-through-[dp],Y for VOLATILE byte loads, expanded post-RA +// to `SEP #$20 ; lda [dp],y ; REP #$20` for the same reason as the store +// above. A byte load is otherwise a 16-bit `lda [dp],y` + mask, which also +// reads address+1: harmless for memory, but for I/O it is a phantom read of +// the neighbouring register (an IIgs SCC data read at $C03A popped channel +// A's receive FIFO at $C03B). A's high byte is left as it was: zextload +// still masks, anyext does not care. +let mayLoad = 1, hasSideEffects = 1, Uses = [Y], Defs = [A, P] in { +def LDB_DPIndLongY : W65816Pseudo<(outs), (ins addrDP:$addr), + "# LDB_DPIndLongY [${addr}], y", []>; +} + // Pointer access with constant offset. `(load (add ptr, $off))` and // `(store val, (add ptr, $off))` come up for struct field access and // array indexing with small constant offsets. Without these patterns, diff --git a/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp b/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp index a81dca8..4b5e304 100644 --- a/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp +++ b/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp @@ -988,7 +988,35 @@ bool W65816StackSlotCleanup::runOnMachineFunction(MachineFunction &MF) { // (t != 0) instead of (i != 0) and runs forever. Opc == W65816::TAX || Opc == W65816::TXA || Opc == W65816::TAY || Opc == W65816::TYA || - Opc == W65816::TXY || Opc == W65816::TYX; + Opc == W65816::TXY || Opc == W65816::TYX || + Opc == W65816::TSC || Opc == W65816::TDC || + Opc == W65816::TSX || + // The byte-move and negate pseudos, for the same reason as the + // i8 logic ops above: all set N/Z, none models Defs=[P]. + // SRL8A / SHL8A expand to XBA ; AND #imm. Live symptom: a + // loop's `if (started && ...) break` lowered to `cmp #1`, then + // five inlined `(uint8_t)(v >> 8)` mailbox byte splits, then + // `bne`. SRL8A was taken as THE test, nothing was wrapped, and + // the BNE read the last AND's Z, so the break never fired + // (JoeyLib's IIgs serial-loss probe, 2026-09-28). + Opc == W65816::SRL8A || Opc == W65816::SHL8A || + Opc == W65816::XBA || Opc == W65816::XBA16 || + Opc == W65816::NEGA16 || Opc == W65816::NEGA8 || + // Every other load, pseudo or already lowered: a load is never + // the test here, any more than LDA_DP above. Custom inserters + // emit real addressing modes before this pass runs (the far- + // pointer load is LDA_DPIndLongY, or the atomic 8-bit + // LDB_DPIndLongY for a volatile byte). + Opc == W65816::LDB_DPIndLongY || + Opc == W65816::LDAabs || Opc == W65816::LDA8abs || + Opc == W65816::LDA8absX || Opc == W65816::LDA8long || + Opc == W65816::LDA_AbsX || Opc == W65816::LDA_AbsY || + Opc == W65816::LDA_DPX || Opc == W65816::LDA_DPInd || + Opc == W65816::LDA_DPIndX || Opc == W65816::LDA_DPIndY || + Opc == W65816::LDA_DPIndLong || Opc == W65816::LDA_DPIndLongY || + Opc == W65816::LDA_LongX || Opc == W65816::LDA_Imm16_Bank || + Opc == W65816::LDX_AbsY || Opc == W65816::LDX_DPY || + Opc == W65816::LDY_AbsX || Opc == W65816::LDY_DPX; }; auto isStackRel = [](unsigned Opc) { // Stack-relative ops read/write at S+disp. PHP decrements S by 1, diff --git a/src/llvm/lib/Target/W65816/W65816TargetMachine.cpp b/src/llvm/lib/Target/W65816/W65816TargetMachine.cpp index a4a5dce..3ec1ad7 100644 --- a/src/llvm/lib/Target/W65816/W65816TargetMachine.cpp +++ b/src/llvm/lib/Target/W65816/W65816TargetMachine.cpp @@ -50,6 +50,7 @@ LLVMInitializeW65816Target() { initializeW65816DAGToDAGISelLegacyPass(PR); initializeW65816StackSlotCleanupPass(PR); initializeW65816SepRepCleanupPass(PR); + initializeW65816FarPtrStageReusePass(PR); initializeW65816BranchExpandPass(PR); initializeW65816TiedDefSpillPass(PR); initializeW65816ABridgeViaXPass(PR); @@ -344,6 +345,10 @@ void W65816PassConfig::addPreEmitPass() { if (!getenv("W65816_DISABLE_STACKOPT")) { addPass(createW65816StackRelToImg()); } + // Last: only now is every instruction concrete (far frames decided, + // stack slots promoted), so the far-pointer staging into $E0..$E3 can be + // tracked across the whole function. Branch distances only shrink. + addPass(createW65816FarPtrStageReuse()); } MachineFunctionInfo *W65816TargetMachine::createMachineFunctionInfo( diff --git a/src/llvm/test/CodeGen/W65816/far-ptr-stage-reuse.mir b/src/llvm/test/CodeGen/W65816/far-ptr-stage-reuse.mir new file mode 100644 index 0000000..6f9ed68 --- /dev/null +++ b/src/llvm/test/CodeGen/W65816/far-ptr-stage-reuse.mir @@ -0,0 +1,465 @@ +# Pin W65816FarPtrStageReuse (pre-emit): the far-pointer staging into DP +# $E0 (224, low word) / $E2 (226, bank word) is deleted only when both words +# provably already hold the value being stored, and the loads, transfers and +# stack temporaries that fed a deleted staging go with it. +# +# The staging shape is the one the LDAptr32 inserter leaves after RA: +# LDA_DP lo ; TAX ; LDA_DP bank ; STA_StackRel t ; TXA ; STA_DP 224 ; +# LDA_StackRel t ; STA_DP 226 +# +# reuse_diamond: staged in the entry block, re-staged in both arms and +# at the join -> only the entry staging survives. +# kept_after_call: a call forgets everything -> re-staging kept. +# kept_other_ptr: a different source word -> kept. +# kept_src_rewritten: the source word is stored in between -> kept. +# kept_dpx_store: a DP-indexed store may hit any DP word -> kept. +# loop_invariant: pointer not written in the loop -> the in-loop staging +# goes (the preheader's still holds on the back edge). +# loop_variant: low word bumped every iteration -> its staging kept, +# the bank word's goes. +# nz_live: a load of the value A already holds stays when a +# branch reads its N/Z. +# stale_liveness: a redundant load at the top of the successor goes, and +# the definition in the predecessor that now reaches its +# user must stay (deleting both used to be one step). +# dse_dead_slot: a stack temporary nothing reads is deleted; a read one +# is kept. +# dse_escape: TSX takes a stack address -> no stack store is touched. +# ret_void: RTL reads only its implicit uses: a staging leftover +# (LDA ; TAX) before a void return is dead. +# ret_i32: RTL implicit $a, implicit $x: X is the high word, kept. +# +# RUN: llc -mtriple=w65816 -run-pass=w65816-farptr-reuse -o - %s | FileCheck %s +--- | + target triple = "w65816" + declare void @g() + define void @reuse_diamond() { ret void } + define void @kept_after_call() { ret void } + define void @kept_other_ptr() { ret void } + define void @kept_src_rewritten() { ret void } + define void @kept_dpx_store() { ret void } + define void @loop_invariant() { ret void } + define void @loop_variant() { ret void } + define void @nz_live() { ret void } + define void @stale_liveness() { ret void } + define void @dse_dead_slot() { ret void } + define void @dse_escape() { ret void } + define void @ret_void() { ret void } + define void @ret_i32() { ret void } +... +--- +# CHECK-LABEL: name: reuse_diamond +# CHECK: bb.0: +# CHECK: STA_DP 224 +# CHECK: STA_DP 226 +# CHECK: bb.1: +# CHECK-NEXT: successors +# CHECK-NEXT: {{^ *$}} +# CHECK-NEXT: LDY_Imm16 2 +# CHECK-NEXT: LDA_DPIndLongY 224 +# CHECK: bb.2: +# CHECK-NEXT: successors +# CHECK-NEXT: {{^ *$}} +# CHECK-NEXT: LDY_Imm16 4 +# CHECK-NEXT: LDA_DPIndLongY 224 +# CHECK: bb.3: +# CHECK-NEXT: STA_DP 214 +# CHECK-NEXT: LDY_Imm16 6 +# CHECK-NEXT: LDA_DPIndLongY 224 +name: reuse_diamond +tracksRegLiveness: false +body: | + bb.0: + successors: %bb.2, %bb.1 + REP 48 + TSC + SEC + SBC_Imm16 16 + TCS + LDA_DP 210 + TAX implicit-def $x, implicit $a + LDA_DP 208 + STA_StackRel 17 + TXA implicit-def $a, implicit $x + STA_DP 224 + LDA_StackRel 17 + STA_DP 226 + LDY_Imm16 0, implicit-def $y + LDA_DPIndLongY 224, implicit-def $a, implicit $y + STA_DP 212 + LDA_DP 200 + BEQ %bb.2, implicit $p + + bb.1: + successors: %bb.3 + LDA_DP 210 + TAX implicit-def $x, implicit $a + LDA_DP 208 + STA_StackRel 13 + TXA implicit-def $a, implicit $x + STA_DP 224 + LDA_StackRel 13 + STA_DP 226 + LDY_Imm16 2, implicit-def $y + LDA_DPIndLongY 224, implicit-def $a, implicit $y + CLC + ADC_DP 212 + BRA %bb.3 + + bb.2: + successors: %bb.3 + LDA_DP 210 + TAX implicit-def $x, implicit $a + LDA_DP 208 + STA_StackRel 11 + TXA implicit-def $a, implicit $x + STA_DP 224 + LDA_StackRel 11 + STA_DP 226 + LDY_Imm16 4, implicit-def $y + LDA_DPIndLongY 224, implicit-def $a, implicit $y + SEC + SBC_DP 212 + + bb.3: + STA_DP 214 + LDA_DP 210 + TAX implicit-def $x, implicit $a + LDA_DP 208 + STA_StackRel 7 + TXA implicit-def $a, implicit $x + STA_DP 224 + LDA_StackRel 7 + STA_DP 226 + LDY_Imm16 6, implicit-def $y + LDA_DPIndLongY 224, implicit-def $a, implicit $y + CLC + ADC_DP 214 + TAY implicit-def $y, implicit $a + TSC + CLC + ADC_Imm16 16 + TCS + TYA implicit-def $a, implicit $y + RTL +... +--- +# CHECK-LABEL: name: kept_after_call +# CHECK: JSLpseudo32 +# CHECK-NEXT: REP 48 +# CHECK-NEXT: LDA_DP 210 +# CHECK-NEXT: STA_DP 224 +# CHECK-NEXT: LDA_DP 208 +# CHECK-NEXT: STA_DP 226 +name: kept_after_call +tracksRegLiveness: false +body: | + bb.0: + REP 48 + LDA_DP 210 + STA_DP 224 + LDA_DP 208 + STA_DP 226 + LDY_Imm16 0, implicit-def $y + LDA_DPIndLongY 224, implicit-def $a, implicit $y + STA_DP 212 + JSLpseudo32 @g + REP 48 + LDA_DP 210 + STA_DP 224 + LDA_DP 208 + STA_DP 226 + LDY_Imm16 2, implicit-def $y + LDA_DPIndLongY 224, implicit-def $a, implicit $y + RTL +... +--- +# The low word changes and is re-staged; the bank word does not. +# CHECK-LABEL: name: kept_other_ptr +# CHECK: LDY_Imm16 0 +# CHECK: LDA_DP 216 +# CHECK-NEXT: STA_DP 224 +# CHECK-NEXT: LDY_Imm16 2 +name: kept_other_ptr +tracksRegLiveness: false +body: | + bb.0: + REP 48 + LDA_DP 210 + STA_DP 224 + LDA_DP 208 + STA_DP 226 + LDY_Imm16 0, implicit-def $y + LDA_DPIndLongY 224, implicit-def $a, implicit $y + STA_DP 212 + LDA_DP 216 + STA_DP 224 + LDA_DP 208 + STA_DP 226 + LDY_Imm16 2, implicit-def $y + LDA_DPIndLongY 224, implicit-def $a, implicit $y + RTL +... +--- +# A already holds what was just stored to 210, so the reload goes too. +# CHECK-LABEL: name: kept_src_rewritten +# CHECK: STA_DP 210 +# CHECK-NEXT: STA_DP 224 +# CHECK-NEXT: LDY_Imm16 2 +name: kept_src_rewritten +tracksRegLiveness: false +body: | + bb.0: + REP 48 + LDA_DP 210 + STA_DP 224 + LDA_DP 208 + STA_DP 226 + LDY_Imm16 0, implicit-def $y + LDA_DPIndLongY 224, implicit-def $a, implicit $y + STA_DP 210 + LDA_DP 210 + STA_DP 224 + LDA_DP 208 + STA_DP 226 + LDY_Imm16 2, implicit-def $y + LDA_DPIndLongY 224, implicit-def $a, implicit $y + RTL +... +--- +# CHECK-LABEL: name: kept_dpx_store +# CHECK: STA_DPX 0 +# CHECK-NEXT: LDA_DP 210 +# CHECK-NEXT: STA_DP 224 +# CHECK-NEXT: LDA_DP 208 +# CHECK-NEXT: STA_DP 226 +name: kept_dpx_store +tracksRegLiveness: false +body: | + bb.0: + REP 48 + LDA_DP 210 + STA_DP 224 + LDA_DP 208 + STA_DP 226 + LDY_Imm16 0, implicit-def $y + LDA_DPIndLongY 224, implicit-def $a, implicit $y + LDX_DP 218 + STA_DPX 0, implicit $a, implicit $x + LDA_DP 210 + STA_DP 224 + LDA_DP 208 + STA_DP 226 + LDY_Imm16 2, implicit-def $y + LDA_DPIndLongY 224, implicit-def $a, implicit $y + RTL +... +--- +# CHECK-LABEL: name: loop_invariant +# CHECK: bb.1: +# CHECK-NEXT: successors +# CHECK-NEXT: {{^ *$}} +# CHECK-NEXT: LDY_Imm16 0 +# CHECK-NEXT: LDA_DPIndLongY 224 +name: loop_invariant +tracksRegLiveness: false +body: | + bb.0: + successors: %bb.1 + REP 48 + LDA_DP 210 + STA_DP 224 + LDA_DP 208 + STA_DP 226 + + bb.1: + successors: %bb.1, %bb.2 + LDA_DP 210 + STA_DP 224 + LDA_DP 208 + STA_DP 226 + LDY_Imm16 0, implicit-def $y + LDA_DPIndLongY 224, implicit-def $a, implicit $y + STA_DP 212 + DEC_DP 214 + BNE %bb.1, implicit $p + + bb.2: + RTL +... +--- +# The low word is bumped with CLC ; ADC #2 (ADC_Imm16 carries TableGen's +# default mayStore, which must not end the bank word's reuse). +# CHECK-LABEL: name: loop_variant +# CHECK: bb.1: +# CHECK-NEXT: successors +# CHECK-NEXT: {{^ *$}} +# CHECK-NEXT: LDA_DP 210 +# CHECK-NEXT: STA_DP 224 +# CHECK-NEXT: LDY_Imm16 0 +name: loop_variant +tracksRegLiveness: false +body: | + bb.0: + successors: %bb.1 + REP 48 + LDA_DP 210 + STA_DP 224 + LDA_DP 208 + STA_DP 226 + + bb.1: + successors: %bb.1, %bb.2 + LDA_DP 210 + STA_DP 224 + LDA_DP 208 + STA_DP 226 + LDY_Imm16 0, implicit-def $y + LDA_DPIndLongY 224, implicit-def $a, implicit $y + STA_DP 212 + LDA_DP 210 + CLC + ADC_Imm16 2 + STA_DP 210 + DEC_DP 214 + BNE %bb.1, implicit $p + + bb.2: + RTL +... +--- +# CHECK-LABEL: name: nz_live +# CHECK: LDA_DP 210 +# CHECK-NEXT: STA_DP 212 +# CHECK-NEXT: LDA_DP 210 +# CHECK-NEXT: BEQ +name: nz_live +tracksRegLiveness: false +body: | + bb.0: + successors: %bb.1, %bb.2 + REP 48 + LDA_DP 210 + STA_DP 212 + LDA_DP 210 + BEQ %bb.2, implicit $p + + bb.1: + successors: %bb.2 + STA_DP 214 + + bb.2: + LDA_Imm16 0, implicit-def $a + RTL +... +--- +# CHECK-LABEL: name: stale_liveness +# CHECK: bb.0: +# CHECK: LDA_DP 210 +# CHECK-NEXT: BRA %bb.1 +# CHECK: bb.1: +# CHECK-NEXT: STA_DP 212 +name: stale_liveness +tracksRegLiveness: false +body: | + bb.0: + successors: %bb.1 + REP 48 + LDA_DP 210 + BRA %bb.1 + + bb.1: + LDA_DP 210 + STA_DP 212 + LDA_DP 216 + RTL +... +--- +# CHECK-LABEL: name: dse_dead_slot +# CHECK: LDA_DP 210 +# CHECK-NEXT: STA_StackRel 3 +# CHECK-NEXT: LDA_DP 216 +# CHECK-NEXT: STA_DP 214 +# CHECK-NEXT: LDA_StackRel 3 +name: dse_dead_slot +tracksRegLiveness: false +body: | + bb.0: + REP 48 + TSC + SEC + SBC_Imm16 4 + TCS + LDA_DP 210 + STA_StackRel 1 + STA_StackRel 3 + LDA_DP 216 + STA_DP 214 + LDA_StackRel 3 + STA_DP 212 + TSC + CLC + ADC_Imm16 4 + TCS + RTL +... +--- +# CHECK-LABEL: name: dse_escape +# CHECK: LDA_DP 210 +# CHECK-NEXT: STA_StackRel 1 +# CHECK-NEXT: STA_StackRel 3 +name: dse_escape +tracksRegLiveness: false +body: | + bb.0: + REP 48 + TSC + SEC + SBC_Imm16 4 + TCS + LDA_DP 210 + STA_StackRel 1 + STA_StackRel 3 + LDA_DP 216 + STA_DP 214 + LDA_StackRel 3 + STA_DP 212 + TSX implicit-def $x, implicit $sp + STX_DP 218 + TSC + CLC + ADC_Imm16 4 + TCS + RTL +... +--- +# CHECK-LABEL: name: ret_void +# CHECK: REP 48 +# CHECK-NEXT: STA_DP 212 +# CHECK-NEXT: RTL +name: ret_void +tracksRegLiveness: false +body: | + bb.0: + REP 48 + STA_DP 212 + LDA_DP 210 + TAX implicit-def $x, implicit $a + RTL +... +--- +# CHECK-LABEL: name: ret_i32 +# CHECK: LDA_DP 210 +# CHECK-NEXT: TAX +# CHECK-NEXT: LDA_DP 208 +# CHECK-NEXT: RTL implicit $a, implicit $x +name: ret_i32 +tracksRegLiveness: false +body: | + bb.0: + REP 48 + LDA_DP 210 + TAX implicit-def $x, implicit $a + LDA_DP 208 + RTL implicit $a, implicit $x +... diff --git a/src/llvm/test/CodeGen/W65816/srl8a-flag-corrupt-wrap.mir b/src/llvm/test/CodeGen/W65816/srl8a-flag-corrupt-wrap.mir new file mode 100644 index 0000000..ea8fa99 --- /dev/null +++ b/src/llvm/test/CodeGen/W65816/srl8a-flag-corrupt-wrap.mir @@ -0,0 +1,112 @@ +# Pin: the byte-move pseudos (SRL8A / SHL8A, which expand to XBA ; AND #imm) +# and every load form count as flag CORRUPTION, not as the flag-defining +# test, in the PHP/PLP wrap (W65816StackSlotCleanup Pass -2.5). +# +# None of them models Defs=[P], so a branch can never take its modelled $p +# from one; while they were missing from the corrupter list, isFlagDefining's +# default claimed them as THE test, the backward walk stopped there and the +# pass declined to wrap. Same family as andi8-flag-corrupt-wrap.mir. +# +# Live symptom: a loop's `if (started && ...) break` lowered to +# cmp #1 ; lda n,s ; xba ; and #$ff ; sta m,s ; (x5) ; bne +# -- five inlined `(uint8_t)(v >> 8)` mailbox byte splits scheduled between +# the compare and its branch. The BNE read the last AND's Z instead of the +# CMP's, so the break never fired (JoeyLib's IIgs serial-loss probe). +# +# srl8a: the reported shape: reload + SRL8A + spill between CMP and BNE. +# The spill after the last corrupter stays outside the wrap. +# shl8a: same for SHL8A. +# far_load: the far-pointer load the LDAptr32 inserter emits (a real +# LDA_DPIndLongY, not a pseudo) between CMP and BEQ; the wrap +# starts at the index load in front of it. +# +# RUN: llc -mtriple=w65816 -run-pass=w65816-stack-slot-cleanup -o - %s | FileCheck %s +--- | + target triple = "w65816" + define void @srl8a() { ret void } + define void @shl8a() { ret void } + define void @far_load() { ret void } +... +--- +# CHECK-LABEL: name: srl8a +# CHECK: CMPi16imm killed $a, 3, implicit-def $p +# CHECK-NEXT: PHP +# CHECK-NEXT: $a = LDAfi %stack.1, 1 +# CHECK-NEXT: $a = SRL8A killed $a +# CHECK-NEXT: PLP +# CHECK-NEXT: STAfi killed $a, %stack.2, 0 +# CHECK-NEXT: BNE %bb.1, implicit $p +name: srl8a +tracksRegLiveness: false +stack: + - { id: 0, size: 2, alignment: 1 } + - { id: 1, size: 2, alignment: 1 } + - { id: 2, size: 2, alignment: 1 } +body: | + bb.0: + successors: %bb.1, %bb.2 + $a = LDAfi %stack.0, 0 + CMPi16imm killed $a, 3, implicit-def $p + $a = LDAfi %stack.1, 0 + $a = SRL8A killed $a + STAfi killed $a, %stack.2, 0, implicit-def $a + BNE %bb.1, implicit $p + BRA %bb.2 + bb.1: + RTL + bb.2: + RTL +... +--- +# CHECK-LABEL: name: shl8a +# CHECK: CMPi16imm killed $a, 5, implicit-def $p +# CHECK-NEXT: PHP +# CHECK-NEXT: $a = LDAfi %stack.1, 1 +# CHECK-NEXT: $a = SHL8A killed $a +# CHECK-NEXT: PLP +# CHECK-NEXT: BEQ %bb.1, implicit $p +name: shl8a +tracksRegLiveness: false +stack: + - { id: 0, size: 2, alignment: 1 } + - { id: 1, size: 2, alignment: 1 } +body: | + bb.0: + successors: %bb.1, %bb.2 + $a = LDAfi %stack.0, 0 + CMPi16imm killed $a, 5, implicit-def $p + $a = LDAfi %stack.1, 0 + $a = SHL8A killed $a + BEQ %bb.1, implicit $p + BRA %bb.2 + bb.1: + RTL + bb.2: + RTL +... +--- +# CHECK-LABEL: name: far_load +# CHECK: CMPi16imm killed $a, 7, implicit-def $p +# CHECK-NEXT: PHP +# CHECK-NEXT: LDY_Imm16 2 +# CHECK-NEXT: LDA_DPIndLongY 224 +# CHECK-NEXT: PLP +# CHECK-NEXT: BEQ %bb.1, implicit $p +name: far_load +tracksRegLiveness: false +stack: + - { id: 0, size: 2, alignment: 1 } +body: | + bb.0: + successors: %bb.1, %bb.2 + $a = LDAfi %stack.0, 0 + CMPi16imm killed $a, 7, implicit-def $p + LDY_Imm16 2, implicit-def $y + LDA_DPIndLongY 224, implicit-def $a, implicit $y + BEQ %bb.1, implicit $p + BRA %bb.2 + bb.1: + RTL + bb.2: + RTL +... diff --git a/src/llvm/test/CodeGen/W65816/volatile-byte-load-8bit.ll b/src/llvm/test/CodeGen/W65816/volatile-byte-load-8bit.ll new file mode 100644 index 0000000..b4678f3 --- /dev/null +++ b/src/llvm/test/CodeGen/W65816/volatile-byte-load-8bit.ll @@ -0,0 +1,76 @@ +; RUN: llc -mtriple=w65816 -O2 < %s | FileCheck %s +; +; Pin: a VOLATILE i8 load through a far pointer is a true 8-bit bus read, +; `sep #$20 ; lda [$e0],y ; rep #$20` (the atomic LDB_DPIndLongY pseudo). +; +; A byte load is otherwise a 16-bit `lda [$e0],y` followed by a mask, which +; also reads address+1. For memory that is harmless and cheaper, so plain +; byte loads keep it; for I/O it is a phantom read of the neighbouring +; register with that register's read side effects. On the IIgs SCC (four +; adjacent registers at $C038-$C03B) a channel-B data read at $C03A popped +; channel A's receive FIFO at $C03B (claudeDocs/LLVM816-ASKS.md #7 in +; JoeyLib). Constant-address byte loads and every byte store were already +; 8-bit. +; +; vol_byte: plain pointer, LDAptr32. +; vol_byte_off: pointer + constant, LDAptr32Off (offset goes in Y). +; vol_sbyte: sign-extending volatile byte load, same 8-bit read. +; plain_byte: non-volatile byte load keeps the 16-bit read + mask. +; vol_word: volatile i16 load is a 16-bit read, no mode switch. + +target datalayout = "e-m:e-p:32:16-i16:16-i32:16-i64:16-f32:16-f64:16-a:8-n8:16-S8" +target triple = "w65816" + +; CHECK-LABEL: vol_byte: +; CHECK: sep #0x20 +; CHECK-NEXT: lda [0xe0], y +; CHECK-NEXT: rep #0x20 +; CHECK-NEXT: and #0xff +define zeroext i8 @vol_byte(ptr %p) { +entry: + %v = load volatile i8, ptr %p, align 1 + ret i8 %v +} + +; CHECK-LABEL: vol_byte_off: +; CHECK: ldy #0x2 +; CHECK-NEXT: sep #0x20 +; CHECK-NEXT: lda [0xe0], y +; CHECK-NEXT: rep #0x20 +define zeroext i8 @vol_byte_off(ptr %p) { +entry: + %a = getelementptr inbounds i8, ptr %p, i32 2 + %v = load volatile i8, ptr %a, align 1 + ret i8 %v +} + +; CHECK-LABEL: vol_sbyte: +; CHECK: sep #0x20 +; CHECK-NEXT: lda [0xe0], y +; CHECK-NEXT: rep #0x20 +define signext i8 @vol_sbyte(ptr %p) { +entry: + %v = load volatile i8, ptr %p, align 1 + ret i8 %v +} + +; CHECK-LABEL: plain_byte: +; CHECK-NOT: sep +; CHECK: lda [0xe0], y +; CHECK-NEXT: and #0xff +define zeroext i8 @plain_byte(ptr %p) { +entry: + %v = load i8, ptr %p, align 1 + ret i8 %v +} + +; CHECK-LABEL: vol_word: +; CHECK-NOT: sep +; CHECK: lda [0xe0], y +; CHECK-NOT: sep +; CHECK: rtl +define zeroext i16 @vol_word(ptr %p) { +entry: + %v = load volatile i16, ptr %p, align 2 + ret i16 %v +} diff --git a/tests/coremark/README.md b/tests/coremark/README.md index c84092d..4ad7244 100644 --- a/tests/coremark/README.md +++ b/tests/coremark/README.md @@ -30,10 +30,13 @@ sanity check on our backend's code-quality. bash tests/coremark/build.sh --layer2 ``` -`--layer2` enables `-mllvm -w65816-dbr-safe-ptrs`. This is **required** -to fit the binary in a single bank; without it, text crosses the IO -window at `0xC000`. CoreMark only touches malloc/static-array memory, -so the dbr-safe-ptrs assumption is correct. +`--layer2` enables `-mllvm -w65816-dbr-safe-ptrs` (smaller, faster +pointer code; correct here because CoreMark only touches malloc/static- +array memory). It is optional: CoreMark plus the runtime it pulls in +(printf, soft-float) no longer fits the ~44 KB below the IO window at +`0xC000` either way, so `runCoreMark.sh` links it multi-segment like the +Lua test (`--segment-cap`, extra segments at bank 4+) and runs it through +`scripts/runMultiSeg.sh`. Default iteration count is 1 (smallest valid run). Override for publishable scores: diff --git a/tests/coremark/coreMark.manifest.json b/tests/coremark/coreMark.manifest.json new file mode 100644 index 0000000..e30cec5 --- /dev/null +++ b/tests/coremark/coreMark.manifest.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "main": "coreMark.bin", + "entry": "__start", + "segments": [ + { + "num": 1, + "name": "SEG1", + "base": "0x001000", + "size": 44929, + "bss_start": "0x00d800", + "bss_size": 6268, + "image": "coreMark.bin", + "entry_offset": "0x0000" + }, + { + "num": 2, + "name": "SEG2", + "base": "0x040000", + "size": 16546, + "bss_start": "0x000000", + "bss_size": 0, + "image": "coreMark.seg2.bin", + "entry_offset": "0x0000" + } + ] +} diff --git a/tests/coremark/runCoreMark.sh b/tests/coremark/runCoreMark.sh index c8a2d14..dfc5f14 100755 --- a/tests/coremark/runCoreMark.sh +++ b/tests/coremark/runCoreMark.sh @@ -16,20 +16,26 @@ cd "$SCRIPT_DIR" rm -rf "$CM_OBJ" bash "$SCRIPT_DIR/build.sh" "$@" -# Link. CoreMark is ~50KB compiled; should fit single bank. +# Link. CoreMark plus the runtime it pulls in (printf, soft-float) is more +# than the ~44 KB bank 0 has below the IO window, so it links multi-segment +# like the Lua test: text packs into 48 KB chunks from bank 0, the rest at +# bank 4+ (runMultiSeg.sh places each segment from the manifest). "$PROJECT_ROOT/tools/link816" -o coreMark.bin --text-base 0x1000 \ + --segment-cap 0xB000 \ + --segment-bank-base 0x040000 \ + --manifest coreMark.manifest.json \ "$RT/crt0.o" \ "$RT/libc.o" "$RT/libgcc.o" "$RT/softFloat.o" "$RT/softDouble.o" \ "$RT/math.o" "$RT/strtol.o" "$RT/snprintf.o" \ "$RT/qsort.o" "$RT/timeExt.o" "$RT/extras.o" \ $CM_OBJ/*.o -ls -la coreMark.bin +ls -la coreMark*.bin echo "" # 0xC0DE sentinel = success; otherwise 0xBEEF = mid-run hang. # Elapsed ticks at 0x025002..0x025005 (low/hi). -MAME_CHECK_FRAME=900 MAME_SECS=30 MAME_TIMEOUT=300 \ - bash "$PROJECT_ROOT/scripts/runInMame.sh" \ - "$SCRIPT_DIR/coreMark.bin" \ +MAME_CHECK_FRAME=900 MAME_SECS=30 MAME_TIMEOUT=300 MAME_RAMSIZE=4M \ + bash "$PROJECT_ROOT/scripts/runMultiSeg.sh" \ + "$SCRIPT_DIR/coreMark.manifest.json" \ --check 0x025000=c0de diff --git a/tests/lua/README.md b/tests/lua/README.md index 0f2b4b6..203cadf 100644 --- a/tests/lua/README.md +++ b/tests/lua/README.md @@ -38,14 +38,20 @@ discussion in [`../../LLVM_65816_DESIGN.md`](../../LLVM_65816_DESIGN.md). ## Runtime execution status -The compiled binary loads under MAME and reaches its entry point -(verified via direct `mame` invocation showing `MAME-READY pc=0x001000`). -Wrapping the run inside `scripts/runMultiSeg.sh` from the agent shell -crashes with SIGSEGV before producing output — this is sandbox-related, -not a code defect. The Lua API smoke test (push 3 ints, sum, check -strings, close state) compiles and links to 5 segments (~200 KB) so a -full Lua interpreter session is feasible if the harness is run in a -plain shell. +`runLuaTest.sh` passes: the API smoke test (create a state, push three +integers, sum them, check a string, pop, close) writes the `0xC0DE` +sentinel under MAME. + +It used to crash, and not because of any sandbox. The link is +multi-segment (`--segment-cap`, five segments), and clang forms a +function pointer as a 16-bit offset plus the bank in DP `$BE` -- bank 0. +link816 only redirected pointers to seg2+ functions through bank-0 `JML` +trampolines for GS/OS ExpressLoad links, so here Lua's allocator (the +first thing `lua_newstate` calls, through a pointer) was called in bank 0, +landed in `vfprintf`, and the machine ran wild; MAME itself then died of +the I/O the runaway stack wrote. link816 now builds the trampolines for +every multi-segment link (fixed bank bases: the absolute target is baked +in). ## Files diff --git a/tests/lua/luaTest.manifest.json b/tests/lua/luaTest.manifest.json index 29146fb..b589d4c 100644 --- a/tests/lua/luaTest.manifest.json +++ b/tests/lua/luaTest.manifest.json @@ -7,7 +7,9 @@ "num": 1, "name": "SEG1", "base": "0x001000", - "size": 42092, + "size": 25189, + "bss_start": "0x008400", + "bss_size": 4248, "image": "luaTest.bin", "entry_offset": "0x0000" }, @@ -15,7 +17,9 @@ "num": 2, "name": "SEG2", "base": "0x040000", - "size": 45028, + "size": 41006, + "bss_start": "0x000000", + "bss_size": 0, "image": "luaTest.seg2.bin", "entry_offset": "0x0000" }, @@ -23,9 +27,21 @@ "num": 3, "name": "SEG3", "base": "0x050000", - "size": 36298, + "size": 43429, + "bss_start": "0x000000", + "bss_size": 0, "image": "luaTest.seg3.bin", "entry_offset": "0x0000" + }, + { + "num": 4, + "name": "SEG4", + "base": "0x060000", + "size": 26491, + "bss_start": "0x000000", + "bss_size": 0, + "image": "luaTest.seg4.bin", + "entry_offset": "0x0000" } ] }