diff --git a/runtime/src/libc.c b/runtime/src/libc.c index 9c93be9..0e5593f 100644 --- a/runtime/src/libc.c +++ b/runtime/src/libc.c @@ -590,13 +590,32 @@ void __heapInitMM(unsigned short userID) { if (bumpPtr) { return; } - // Heap size: keep the link-time window when it is in a sane range, - // else a 32 KB default. The window is bank-0-bounded, so it never - // spans more than a single bank. + // Heap size to ask the Memory Manager for. + // + // The link-time window is the space left in the ENTRY BANK above bss. + // That is the right answer for the fallback path below (where malloc + // really does run in that window) but it is NOT a statement of how + // much heap the program wants: this handle carries no attrBank, so + // the MM may place it in any bank and its size is not bounded by what + // the entry bank had left. So the window is a floor to beat, not the + // request itself -- take whichever of it and the default is larger. + // + // Using the window directly used to be masked by it being tiny: it + // was capped at $BF00 to dodge a bank-0 IO window the program does + // not live in, so most programs' windows fell under the old 0x2000 + // floor and silently got the 32 KB default. Widening the window to + // the real top of the bank (link816's __heap_start comment) put those + // windows IN range, which SHRANK the request -- AGI went from 32 KB + // to 20 KB and stopped drawing. Clamping up instead of down means no + // program can ever get a smaller heap than it did before. + #define HEAP_MM_DEFAULT 0x8000UL // 32 KB + #define HEAP_MM_MAX 0xC000UL // 48 KB; must still fit one bank unsigned long want = (unsigned long)(void *)__heap_end - (unsigned long)(void *)__heap_start; - if (want < 0x2000UL || want > 0xC000UL) { - want = 0x8000UL; // 32 KB + if (want < HEAP_MM_DEFAULT) { + want = HEAP_MM_DEFAULT; + } else if (want > HEAP_MM_MAX) { + want = HEAP_MM_MAX; } // attr 0x8014 = attrLocked | attrNoCross | attrPage. Every bit here is // set in the proven-good desktop.c DP-block value 0xC015, minus diff --git a/src/link816/link816.cpp b/src/link816/link816.cpp index b066691..81059df 100644 --- a/src/link816/link816.cpp +++ b/src/link816/link816.cpp @@ -919,6 +919,30 @@ struct Linker { // bank's tiny budget to them wastes ~50K per bank. 0 = same as // segmentCap (the historical behaviour). uint32_t overflowCap = 0; + + // Set by --full-bank: this image owns a whole 64K bank, so there is + // no $C000-$CFFF IO window and no $D000+ language card to dodge, and + // text / rodata / .init_array / BSS may use the bank right up to the + // ceiling. + // + // That is true of a GS/OS OMF image: the Loader asks the Memory + // Manager for each bank-aligned segment and gets whatever is free + // (measured under MAME: joeylib's examples run in banks $08-$0B, and + // bank $00 appears only in ROM and GS/OS glue). Those banks are plain + // RAM throughout. + // + // It is NOT true of an image raw-loaded into bank 0, which is what + // scripts/runInMame.sh does (loads at $00:1000 and jumps) for the + // smoke, bench and probe scripts. There the hazards are real and + // measured: smoke #86 had string literals read back as IO register + // values, and smoke #128 had BSS clearing hit soft switches and crash + // MAME. + // + // DEFAULT OFF, i.e. assume bank-0 hazards. The conservative layout is + // merely smaller, while guessing "full bank" for an image that really + // does land in bank 0 corrupts it -- so every raw-load consumer keeps + // working untouched and only the OMF driver opts in. + bool fullBank = false; uint32_t segmentBankBase = 0x040000; std::string manifestPath; // Phase C.2 — opt-in for ExpressLoad-multi-seg cINTERSEG emission. @@ -1479,9 +1503,15 @@ struct Linker { // below the window). The small trampoline table may use that // margin; an actual IO-window crossing is still caught here // (and again by the textSize check below). - if (segSizes[0] > kIoWindowStart) - die("seg 1 text + cross-segment trampoline table crosses " - "the IIgs IO window 0xC000"); + uint32_t textCeiling = fullBank ? kBank0Ceiling + : kIoWindowStart; + if (segSizes[0] > textCeiling) { + char msg[160]; + std::snprintf(msg, sizeof(msg), + "seg 1 text + cross-segment trampoline table (%u " + "bytes) overruns 0x%X", segSizes[0], textCeiling); + die(msg); + } std::fprintf(stderr, "link816: %zu cross-seg function trampolines (%u bytes) " "in seg 1\n", gTrampList.size(), @@ -1527,7 +1557,7 @@ struct Linker { // would fetch instructions from hardware registers. Programs // that grow this big need to split into bank 1 (not currently // supported by this linker). - if (overlapsIoWindow(L.textBase, L.textSize) && + if (!fullBank && overlapsIoWindow(L.textBase, L.textSize) && L.textBase < kIoWindowStart) { char msg[160]; std::snprintf(msg, sizeof(msg), @@ -1546,7 +1576,8 @@ struct Linker { // returned garbage because the keys read back as IO register // values). Catches both "starts before IO, crosses in" and // "starts inside IO" cases. - if (!rodataBase && overlapsIoWindow(L.rodataBase, L.rodataSize)) { + if (!fullBank && !rodataBase && + overlapsIoWindow(L.rodataBase, L.rodataSize)) { // Page-align upward past the IO window. L.rodataBase = kIoWindowEnd; // Pad the image so the gap between text-end and rodata- @@ -1559,7 +1590,7 @@ struct Linker { L.initSize = curInit; // Init_array can also land in IO if rodata ends just before // or starts inside. - if (overlapsIoWindow(L.initBase, L.initSize)) { + if (!fullBank && overlapsIoWindow(L.initBase, L.initSize)) { L.initBase = kIoWindowEnd; } // After all skips, sanity-check we haven't gone past the LC @@ -1609,7 +1640,8 @@ struct Linker { if (L.bssBase < loadEnd) { // Page-align upward for nicer addresses in the map. L.bssBase = (loadEnd + 0xFF) & ~0xFFu; - if (L.bssBase >= kIoWindowStart && L.bssBase < kIoWindowEnd) { + if (!fullBank && L.bssBase >= kIoWindowStart && + L.bssBase < kIoWindowEnd) { L.bssBase = kIoWindowEnd; } } @@ -1618,7 +1650,7 @@ struct Linker { // BSS writes to the IO range hit soft switches — caught // by smoke #128 hex dumper, where ~954-byte BSS pushed // past kIoWindowStart and BSS-clear writes crashed MAME. - if (overlapsIoWindow(L.bssBase, L.bssSize)) { + if (!fullBank && overlapsIoWindow(L.bssBase, L.bssSize)) { L.bssBase = kIoWindowEnd; } if (L.bssBase + L.bssSize > kBank0Ceiling) { @@ -1723,48 +1755,46 @@ struct Linker { globalSyms[sizeNames[i]] = 0; } } - // __heap_start / __heap_end: pick the largest contiguous safe - // range above bss_end. Without this, the previous hardcoded - // heap_end=$BF00 gave heap_end < heap_start whenever BSS - // spilled into LC1 — malloc immediately returned NULL. - // If bank-0 heap would be tiny (<512B) push to LC1 (just past - // the IO window). + // __heap_start / __heap_end: everything from bss_end to the top + // of the entry segment's BANK. // - // Bank-0 heap top sits one page below the IO window so heap - // alloc bumps never touch soft switches. kIoWindowStart - 0x100 - // = $BF00; encoded here for clarity rather than as a raw - // constant. - constexpr uint32_t kBank0HeapTop = kIoWindowStart - 0x100; // $BF00 - constexpr uint32_t MIN_HEAP = 512; - // Prefer a bank-0 heap below the IO window. Pushing the heap into the - // bank-0 LC ($D000-$FFFF) is a LAST RESORT and unreliable: GS/OS uses - // that RAM, corrupting heap-resident state (a surface struct landing - // there had its palette read back as garbage -- both the $D000-$DFFF - // bank-switched half AND the $E000-$FFFF common half are unsafe). The - // driver (clang-build.sh) avoids it by lowering the multi-seg - // --segment-cap until rodata + BSS + heap fit below the IO window; this - // LC fallback only fires for a program too large for even that. + // There is no IO window and no language card to dodge here. Those + // exist only in banks $00/$01/$E0/$E1; GS/OS places bank-aligned + // OMF segments in free banks (measured under MAME: joeylib's + // examples run in $08-$0B, and bank $00 shows up only in ROM and + // GS/OS glue), and crt0Gsos points DBR at the entry segment's + // bank, so these offsets address plain RAM across the whole 64K. + // + // The old model capped the heap at $BF00 ("kBank0HeapTop") and + // fell back to a bank-0 language-card range, which cost ~16K of + // the entry bank and drove clang-build.sh to shrink --segment-cap + // to fit rodata + BSS + heap under a window that is not there. It + // also justified itself with "crt0's $C083 read-twice enables + // read+write", but crt0Gsos.s does no language-card enable at all + // ("GS/OS configures memory"). + // + // What this window actually does in a program that links libc: + // it is a SIZE REQUEST. libc's __heapInitMM asks the Memory + // Manager for (heap_end - heap_start) bytes and malloc runs + // inside that handle, wherever the MM places it. The window is + // only addressed directly if that allocation fails, and on that + // path it is still in the entry bank, so it is still plain RAM. + // + // 0xFFFE rather than 0x10000: relocation patching at the use site + // is 16-bit, so 0x10000 truncates to 0 and malloc would compute + // heap_end < heap_start and return NULL for everything. + constexpr uint32_t kEntryBankHeapTop = 0xFFFE; uint32_t heapStart = L.bssBase + L.bssSize; - if (heapStart >= kBank0HeapTop && heapStart < kIoWindowEnd) { - heapStart = kIoWindowEnd; // skip IO window + tiny tail - } else if (heapStart < kBank0HeapTop && - (kBank0HeapTop - heapStart) < MIN_HEAP) { - heapStart = kIoWindowEnd; // bank-0 sliver too small; use LC + uint32_t heapEnd = kEntryBankHeapTop; + if (heapStart >= heapEnd) { + // BSS filled the bank (it may span several). No link-time + // window is possible; libc's __heapInitMM sees a zero-size + // request and applies its own 32K default, which is the best + // available answer. + heapEnd = heapStart; } globalSyms["__heap_start"] = heapStart; - if (heapStart < kIoWindowStart) { - globalSyms["__heap_end"] = kBank0HeapTop; - } else if (heapStart < kBank0Ceiling) { - // Heap in LC area ($D000-$FFFF). crt0's $C083 read-twice - // enables read+write for the whole range. Cap at 0xFFFE - // (not kBank0Ceiling) — relocation patching at the use - // site is 16-bit and 0x10000 truncates to 0; malloc would - // then think heap_end < heap_start and return NULL. - globalSyms["__heap_end"] = 0xFFFE; - } else { - // Unreachable — bssBase + bssSize > kBank0Ceiling check above. - globalSyms["__heap_end"] = heapStart; - } + globalSyms["__heap_end"] = heapEnd; // 2. Build global symbol map. Honor weak vs strong binding: // - strong def overrides any prior weak def @@ -2446,6 +2476,9 @@ int main(int argc, char **argv) { } else if (a == "--segment-cap") { if (++i >= argc) usage(argv[0]); linker.segmentCap = parseInt(argv[i++]); + } else if (a == "--full-bank") { + linker.fullBank = true; + i++; } else if (a == "--overflow-cap") { if (++i >= argc) usage(argv[0]); linker.overflowCap = parseInt(argv[i++]); diff --git a/src/llvm/lib/Target/W65816/W65816AsmPrinter.cpp b/src/llvm/lib/Target/W65816/W65816AsmPrinter.cpp index 98f0297..a3224f9 100644 --- a/src/llvm/lib/Target/W65816/W65816AsmPrinter.cpp +++ b/src/llvm/lib/Target/W65816/W65816AsmPrinter.cpp @@ -509,6 +509,16 @@ void W65816AsmPrinter::emitInstruction(const MachineInstr *MI) { // non-zero so the null test passes and the direct call uses the // symbol's own reloc, not this bank byte). Defined/normal globals // and compiler external symbols (libgcc helpers) keep `lda $BE`. + // + // CODE symbols in a multi-segment OMF app are NOT an exception, even + // though every overflow segment lives in its own bank: link816 + // redirects the 16-bit half of `&fn` for a function outside the + // entry segment to a `JML fn` trampoline it appends to the ENTRY + // segment (see the cross-segment function-pointer trampoline table + // in link816.cpp), so the entry bank from $BE is exactly right for + // that offset. A per-function bank slot read from .data was tried + // on 2026-09-22 and broke every such pointer (bank of the callee + + // offset of the trampoline). Do not resurrect it. const MachineOperand &Sym = MI->getOperand(1); bool WeakUndef = Sym.isGlobal() && Sym.getGlobal()->hasExternalWeakLinkage(); diff --git a/src/llvm/lib/Target/W65816/W65816ISelLowering.cpp b/src/llvm/lib/Target/W65816/W65816ISelLowering.cpp index e04006e..f4fd311 100644 --- a/src/llvm/lib/Target/W65816/W65816ISelLowering.cpp +++ b/src/llvm/lib/Target/W65816/W65816ISelLowering.cpp @@ -4082,3 +4082,14 @@ W65816TargetLowering::EmitInstrWithCustomInserter(MachineInstr &MI, } } } + + +void W65816TargetLowering::computeKnownBitsForFrameIndex(int FIOp, + KnownBits &Known, + const MachineFunction &MF) const { + // See the header: stack objects carry no realised alignment here, so + // nothing about the address is known. + (void)FIOp; + (void)MF; + (void)Known; +} diff --git a/src/llvm/lib/Target/W65816/W65816ISelLowering.h b/src/llvm/lib/Target/W65816/W65816ISelLowering.h index 84ab883..e026fb8 100644 --- a/src/llvm/lib/Target/W65816/W65816ISelLowering.h +++ b/src/llvm/lib/Target/W65816/W65816ISelLowering.h @@ -39,6 +39,16 @@ public: SDValue LowerCall(TargetLowering::CallLoweringInfo &CLI, SmallVectorImpl &InVals) const override; + // A frame index promises NO known-zero low bits: the 65816 stack pointer + // is whatever it is (JSL pushes 3 bytes, PHP 1), so a stack object's + // alignment is never realised in its address. The default derives known + // bits from MachineFrameInfo's object alignment, which let DAGCombine + // fold `slot + 1` into `slot | 1` -- a volatile union's byte reads then + // hit the wrong byte whenever the frame base was odd (Space Taxi's + // collision masks, 2026-09-21). + void computeKnownBitsForFrameIndex(int FIOp, KnownBits &Known, + const MachineFunction &MF) const override; + SDValue LowerReturn(SDValue Chain, CallingConv::ID CallConv, bool IsVarArg, const SmallVectorImpl &Outs, const SmallVectorImpl &OutVals, const SDLoc &DL, diff --git a/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp b/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp index e297ef8..a81dca8 100644 --- a/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp +++ b/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp @@ -941,9 +941,29 @@ bool W65816StackSlotCleanup::runOnMachineFunction(MachineFunction &MF) { // real CMP. NOTE: the *fi variants (ANDfi/ORAfi/EORfi) and // ADC/SBC DO model Defs=[P] and can legitimately be the branch's // flag source, so they are handled by $p liveness and are NOT - // listed here; same for INA/DEA and the shifts, which the DEC/ - // INC and shift-carry idioms may read directly. + // listed here; same for the shifts, which the shift-carry idiom + // reads directly. Opc == W65816::EORi16imm || Opc == W65816::EORabs || + // INC/DEC in every form. None of them models Defs=[P] either, + // so at this pass's point no branch can take its flags from + // one: the `dec a / bne` idiom only exists AFTER SepRepCleanup + // folds ADC #±1 (Defs=[P]) into INA/DEA and drops the `cmp #0`, + // both of which run later. Left out, isFlagDefining's default + // made a loop's own `r++` the "test": `for (r = 0; r < 21; r++)` + // lowered to `cmp #end ; inc a ; bne loop`, the INA trampled the + // CMP's Z, and the loop only stopped when the counter wrapped + // through zero -- Space Taxi's cab-vs-background test walked 51 + // rows of a 21-row sprite into the next cel's data and crashed + // the IIgs attract demo on a landing (2026-09-21). + Opc == W65816::INA_PSEUDO || Opc == W65816::DEA_PSEUDO || + Opc == W65816::INA_PSEUDO8 || Opc == W65816::DEA_PSEUDO8 || + Opc == W65816::INA || Opc == W65816::DEA || + Opc == W65816::INX || Opc == W65816::DEX || + Opc == W65816::INY || Opc == W65816::DEY || + Opc == W65816::INC_DP || Opc == W65816::INC_Abs || + Opc == W65816::INC_DPX || Opc == W65816::INC_AbsX || + Opc == W65816::DEC_DP || Opc == W65816::DEC_Abs || + Opc == W65816::DEC_DPX || Opc == W65816::DEC_AbsX || Opc == W65816::ANDi16imm || Opc == W65816::ANDabs || Opc == W65816::ORAi16imm || Opc == W65816::ORAabs || // ...and the i8 siblings, for exactly the same reason (cf. diff --git a/src/llvm/test/CodeGen/W65816/frameindex-no-known-bits.ll b/src/llvm/test/CodeGen/W65816/frameindex-no-known-bits.ll new file mode 100644 index 0000000..fef87c1 --- /dev/null +++ b/src/llvm/test/CodeGen/W65816/frameindex-no-known-bits.ll @@ -0,0 +1,20 @@ +; Pin: a frame index has no known-zero low bits (W65816TargetLowering:: +; computeKnownBitsForFrameIndex). The 65816 stack pointer carries no +; alignment (JSL pushes 3 bytes, PHP 1), so the default known bits from the +; object's alignment let DAGCombine fold `slot + 1` into `slot | 1`: a +; volatile union's second byte was read from the FIRST byte whenever the +; frame base was odd. JoeyLib's Space Taxi lost its collision masks that +; way (2026-09-21). +; +; RUN: llc -mtriple=w65816 -O2 < %s | FileCheck %s +; CHECK-LABEL: byte1: +; CHECK-NOT: ora #0x1 +; CHECK: rtl +define i8 @byte1(i32 %v) { +entry: + %u = alloca i32, align 2 + store volatile i32 %v, ptr %u, align 2 + %p1 = getelementptr inbounds i8, ptr %u, i16 1 + %b = load volatile i8, ptr %p1, align 1 + ret i8 %b +} diff --git a/src/llvm/test/CodeGen/W65816/ina-flag-corrupt-wrap.mir b/src/llvm/test/CodeGen/W65816/ina-flag-corrupt-wrap.mir new file mode 100644 index 0000000..79d8982 --- /dev/null +++ b/src/llvm/test/CodeGen/W65816/ina-flag-corrupt-wrap.mir @@ -0,0 +1,100 @@ +# Pin: INC/DEC in every form (INA_PSEUDO / DEA_PSEUDO, their i8 twins, the +# raw INA/DEA/INX/DEX/INY/DEY and the memory INC/DEC) 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 at this pass's point no branch can +# legitimately take its flags from one -- the `dec a / bne` idiom only +# exists after SepRepCleanup folds ADC #+-1 into INA/DEA and drops the +# `cmp #0`, both of which run later. While they were missing, +# isFlagDefining's default claimed a loop's own increment as THE test: +# the backward walk from the Bxx stopped at the INA and declined to wrap. +# +# Live symptom: `for (r = 0; r < 21; r++) { ... }` lowered to +# cmp #end ; inc a ; bne loop +# so the branch read the INC's Z, not the CMP's, and the loop only ended +# when the counter wrapped through zero. JoeyLib's Space Taxi walked 51 +# rows of a 21-row sprite into the next cel's data and crashed the IIgs +# attract demo on a landing. +# +# ina_i16: the reported shape -- INA_PSEUDO between CMP and BNE. +# dea_i8: DEA_PSEUDO8 between CMP and BEQ. +# no_test: an INA_PSEUDO with no test before it gets no wrap (nothing to +# preserve, and the wrap costs 8 cycles). +# +# RUN: llc -mtriple=w65816 -run-pass=w65816-stack-slot-cleanup -o - %s | FileCheck %s +--- | + target triple = "w65816" + define void @ina_i16() { ret void } + define void @dea_i8() { ret void } + define void @no_test() { ret void } +... +--- +# CHECK-LABEL: name: ina_i16 +# CHECK: CMPi16imm killed $a, 65506, implicit-def $p +# CHECK-NEXT: PHP +# CHECK-NEXT: $a = INA_PSEUDO $a +# CHECK-NEXT: PLP +# CHECK-NEXT: BNE %bb.1, implicit $p +name: ina_i16 +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, 65506, implicit-def $p + $a = INA_PSEUDO $a + BNE %bb.1, implicit $p + BRA %bb.2 + bb.1: + RTL + bb.2: + RTL +... +--- +# CHECK-LABEL: name: dea_i8 +# CHECK: CMPi16imm killed $a, 7, implicit-def $p +# CHECK-NEXT: PHP +# CHECK-NEXT: $a = DEA_PSEUDO8 $a +# CHECK-NEXT: PLP +# CHECK-NEXT: BEQ %bb.1, implicit $p +name: dea_i8 +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 + $a = DEA_PSEUDO8 $a + BEQ %bb.1, implicit $p + BRA %bb.2 + bb.1: + RTL + bb.2: + RTL +... +--- +# CHECK-LABEL: name: no_test +# CHECK-NOT: PHP +# CHECK: $a = INA_PSEUDO $a +# CHECK-NEXT: BNE %bb.1, implicit $p +name: no_test +tracksRegLiveness: false +stack: + - { id: 0, size: 2, alignment: 1 } +body: | + bb.0: + successors: %bb.1, %bb.2 + $a = LDAfi %stack.0, 0 + $a = INA_PSEUDO $a + BNE %bb.1, implicit $p + BRA %bb.2 + bb.1: + RTL + bb.2: + RTL +...