diff --git a/src/link816/link816.cpp b/src/link816/link816.cpp index e383fab..b066691 100644 --- a/src/link816/link816.cpp +++ b/src/link816/link816.cpp @@ -912,6 +912,13 @@ struct Linker { // bytes per text segment; overflow text sections get bank-aligned // bases starting at segmentBankBase. uint32_t segmentCap = 0; + // Cap for OVERFLOW segments (seg 2+). --segment-cap exists to bound + // how much text stays in the ENTRY bank, which also has to hold + // rodata, bss and the heap under the $C000 IO window. Overflow banks + // carry text ONLY and have the full 64K free, so applying the entry + // bank's tiny budget to them wastes ~50K per bank. 0 = same as + // segmentCap (the historical behaviour). + uint32_t overflowCap = 0; uint32_t segmentBankBase = 0x040000; std::string manifestPath; // Phase C.2 — opt-in for ExpressLoad-multi-seg cINTERSEG emission. @@ -1295,8 +1302,12 @@ struct Linker { while (true) { if (curSeg > segSizes.size()) segSizes.push_back(0); if (segSizes[curSeg - 1] == 0) break; - if (!segmentCap) break; - if (segSizes[curSeg - 1] + sz <= segmentCap) break; + // Entry bank shares its 64K with rodata/bss/heap; + // overflow banks hold text only. + uint32_t capHere = (curSeg == 1 || overflowCap == 0) + ? segmentCap : overflowCap; + if (!capHere) break; + if (segSizes[curSeg - 1] + sz <= capHere) break; curSeg++; } } else if (curSeg > segSizes.size()) { @@ -2435,6 +2446,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 == "--overflow-cap") { + if (++i >= argc) usage(argv[0]); + linker.overflowCap = parseInt(argv[i++]); } else if (a == "--segment-bank-base") { if (++i >= argc) usage(argv[0]); linker.segmentBankBase = parseInt(argv[i++]); diff --git a/src/llvm/lib/Target/W65816/W65816RegisterInfo.cpp b/src/llvm/lib/Target/W65816/W65816RegisterInfo.cpp index 99df096..a0e12d1 100644 --- a/src/llvm/lib/Target/W65816/W65816RegisterInfo.cpp +++ b/src/llvm/lib/Target/W65816/W65816RegisterInfo.cpp @@ -92,10 +92,36 @@ static bool expandFarFI(MachineInstr &MI, int FPOff, return true; } case W65816::STAfi: { + // Non-A source: the value has to travel through A to reach the + // store, so PHA-bracket it exactly like the stack-relative path in + // eliminateFrameIndex below. STAfi is documented (and relied on) + // as A-preserving for every source class -- W65816StackSlotCleanup + // walks past a STAfi to a different slot without invalidating its + // "A still holds slot N" fact, on the strength of that bracket. + // Without it here, an IMG-source STAfi in a >0xFF frame silently + // clobbered A and the cleanup then deleted the *following* reload, + // so the next `sta $E0` parked the source's value where the caller + // wanted something else. Live symptom: the LDAptr32S inserter + // built its far pointer with the pointer's BANK byte in the offset + // half, and `lda [$E0],y` read a stray address -- Space Taxi's + // stSimTick read a nonzero byte for sim->collisionPhase and never + // ran its physics block. + // + // Unlike the ,s path no offset fixup is needed: the store goes + // through the DP frame pointer ($F6), which PHA does not move. Register Src = MI.getOperand(0).getReg(); int srcDP = imgRegToDP(Src); - if (srcDP >= 0) + bool ViaA = srcDP >= 0 || Src == W65816::X || Src == W65816::Y; + if (ViaA) + BuildMI(MBB, II, DL, TII.get(W65816::PHA)); + if (srcDP >= 0) { BuildMI(MBB, II, DL, TII.get(W65816::LDA_DP)).addImm(srcDP); + } else if (Src == W65816::X) { + BuildMI(MBB, II, DL, TII.get(W65816::TXA)); + } else if (Src == W65816::Y) { + // Before the STY_DP/LDY below overwrite Y. + BuildMI(MBB, II, DL, TII.get(W65816::TYA)); + } BuildMI(MBB, II, DL, TII.get(W65816::STY_DP)).addImm(0xFA) .addReg(W65816::Y, RegState::Implicit | RegState::Undef); BuildMI(MBB, II, DL, TII.get(W65816::LDY_Imm16)).addImm(FPOff); @@ -104,6 +130,8 @@ static bool expandFarFI(MachineInstr &MI, int FPOff, .addReg(W65816::A, RegState::Implicit) .addReg(W65816::Y, RegState::Implicit); BuildMI(MBB, II, DL, TII.get(W65816::LDY_DP)).addImm(0xFA); + if (ViaA) + BuildMI(MBB, II, DL, TII.get(W65816::PLA)); return true; } case W65816::STA8fi: { diff --git a/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp b/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp index f43f559..e297ef8 100644 --- a/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp +++ b/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp @@ -946,6 +946,20 @@ bool W65816StackSlotCleanup::runOnMachineFunction(MachineFunction &MF) { Opc == W65816::EORi16imm || Opc == W65816::EORabs || Opc == W65816::ANDi16imm || Opc == W65816::ANDabs || Opc == W65816::ORAi16imm || Opc == W65816::ORAabs || + // ...and the i8 siblings, for exactly the same reason (cf. + // LDAi16imm_bank above): none of the six model Defs=[P], so a + // branch can never legitimately take its flags from one, and + // leaving them out made isFlagDefining's default claim them as + // THE test -- the walk stopped early and declined to wrap. + // Live symptom: `if (a->f) { if (b & 1) ... }` lowered to + // `cmp #0 ; lda n,s ; and #1 ; beq`, where the AND was not a + // test at all but the value the successor block consumes. The + // BEQ read the AND's Z instead of the CMP's, so the outer test + // never fired -- JoeyLib's Space Taxi never cleared its + // landed-on-pad latch and the attract demo parked forever. + Opc == W65816::EORi8imm || + Opc == W65816::ANDi8imm || + Opc == W65816::ORAi8imm || // Register transfers — TAX/TXA/TAY/TYA/TXY/TYX update N/Z // based on the transferred value. They're "data movement" // not "comparison"; treat as corruption so the wrap pass diff --git a/src/llvm/test/CodeGen/W65816/andi8-flag-corrupt-wrap.mir b/src/llvm/test/CodeGen/W65816/andi8-flag-corrupt-wrap.mir new file mode 100644 index 0000000..c07cfec --- /dev/null +++ b/src/llvm/test/CodeGen/W65816/andi8-flag-corrupt-wrap.mir @@ -0,0 +1,112 @@ +# Pin: the i8 logic pseudos (ANDi8imm / ORAi8imm / EORi8imm) count as +# flag CORRUPTION, not as the flag-defining test, in the PHP/PLP wrap +# (W65816StackSlotCleanup Pass -2.5). +# +# None of ANDi8imm/ORAi8imm/EORi8imm model Defs=[P], so a branch can never +# legitimately take its flags from one -- exactly the argument that already +# puts their i16 siblings in the corrupter list. While they were missing, +# isFlagDefining's default claimed them as THE test: the backward walk from +# the Bxx stopped at the AND, saw nothing corrupting between it and the +# branch, and declined to wrap. The CMP's flags were then trampled by the +# reload sitting in front of the AND. +# +# Live symptom: `if (a->latch) { if (b & 1) a->latch = 0; }` lowered to +# cmp #0 ; lda n,s ; and #1 ; beq +# where the AND is not a test at all -- it computes the value the successor +# block consumes. The BEQ read the AND's Z instead of the CMP's, so the outer +# test never fired. JoeyLib's Space Taxi never cleared its landed-on-pad +# latch, the cab never took off again, and the attract demo parked on the +# first pad forever instead of playing its recording out. +# +# The compares below use a NON-ZERO immediate on purpose: after an AND or a +# load, `cmp #0` is redundant (both already set Z) and the pass folds it away, +# which would make the pin ambiguous about which instruction the branch reads. +# +# and_i8: the reported shape -- reload + ANDi8imm between CMP and BEQ. +# ora_i8: same for ORAi8imm. +# no_corrupt: a bare CMP/BEQ pair still gets no wrap (the wrap costs 8 cycles; +# don't pay it where nothing corrupts). +# +# RUN: llc -mtriple=w65816 -run-pass=w65816-stack-slot-cleanup -o - %s | FileCheck %s +--- | + target triple = "w65816" + define void @and_i8() { ret void } + define void @ora_i8() { ret void } + define void @no_corrupt() { ret void } +... +--- +# CHECK-LABEL: name: and_i8 +# CHECK: CMPi16imm killed $a, 3, implicit-def $p +# CHECK-NEXT: PHP +# CHECK-NEXT: $a = LDAfi %stack.1, 1 +# CHECK-NEXT: $a = ANDi8imm killed $a, 1 +# CHECK-NEXT: PLP +# CHECK-NEXT: BEQ %bb.1, implicit $p +name: and_i8 +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, 3, implicit-def $p + $a = LDAfi %stack.1, 0 + $a = ANDi8imm killed $a, 1 + BEQ %bb.1, implicit $p + BRA %bb.2 + bb.1: + RTL + bb.2: + RTL +... +--- +# CHECK-LABEL: name: ora_i8 +# CHECK: CMPi16imm killed $a, 5, implicit-def $p +# CHECK-NEXT: PHP +# CHECK-NEXT: $a = LDAfi %stack.1, 1 +# CHECK-NEXT: $a = ORAi8imm killed $a, 1 +# CHECK-NEXT: PLP +# CHECK-NEXT: BNE %bb.1, implicit $p +name: ora_i8 +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 = ORAi8imm killed $a, 1 + BNE %bb.1, implicit $p + BRA %bb.2 + bb.1: + RTL + bb.2: + RTL +... +--- +# Nothing corrupting between the test and the branch: no wrap, no cost. +# CHECK-LABEL: name: no_corrupt +# CHECK: CMPi16imm killed $a, 7, implicit-def $p +# CHECK-NEXT: BEQ %bb.1, implicit $p +# CHECK-NOT: PHP +name: no_corrupt +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 + BEQ %bb.1, implicit $p + BRA %bb.2 + bb.1: + RTL + bb.2: + RTL +... diff --git a/src/llvm/test/CodeGen/W65816/stafi-far-nona-pha-bracket.mir b/src/llvm/test/CodeGen/W65816/stafi-far-nona-pha-bracket.mir new file mode 100644 index 0000000..3b645c0 --- /dev/null +++ b/src/llvm/test/CodeGen/W65816/stafi-far-nona-pha-bracket.mir @@ -0,0 +1,79 @@ +# Pin: a STAfi whose source is an IMGn (or X/Y) register must PHA-bracket the +# transfer through A on BOTH frame paths -- the stack-relative one and the far +# `($F6),Y` one taken when the slot's offset exceeds 0xFF. +# +# STAfi is documented, and relied upon, as A-preserving for every source class: +# W65816StackSlotCleanup walks past a STAfi to a DIFFERENT slot without +# invalidating its "A still holds slot N" fact, on the strength of that bracket. +# The far path used to emit a bare `LDA dp`, so an IMG-source STAfi in a >0xFF +# frame silently clobbered A and the cleanup then deleted the FOLLOWING reload. +# Live symptom: the LDAptr32S inserter built its far pointer with the pointer's +# BANK byte in the offset half, so `lda [$E0],y` read a stray address -- JoeyLib's +# Space Taxi read a nonzero byte for a zero struct field and never ran its +# physics. +# +# far_img: offset > 0xFF, IMG source -> PHA / LDA dp / store / PLA. +# near_img: offset <= 0xFF, IMG source -> the long-standing ,s bracket. +# far_acc: offset > 0xFF, A source -> NO bracket (A is the source; the +# +7 cycles would be pure cost). +# +# RUN: llc -mtriple=w65816 -run-pass=prologepilog -o - %s | FileCheck %s +--- | + target triple = "w65816" + define void @far_img() { ret void } + define void @near_img() { ret void } + define void @far_acc() { ret void } +... +--- +# CHECK-LABEL: name: far_img +# CHECK: PHA +# CHECK-NEXT: LDA_DP 192 +# CHECK-NEXT: STY_DP 250 +# CHECK-NEXT: LDY_Imm16 +# CHECK-NEXT: STA_DPIndLongY 246 +# CHECK-NEXT: LDY_DP 250 +# CHECK-NEXT: PLA +name: far_img +tracksRegLiveness: false +stack: + - { id: 0, size: 2, alignment: 1 } + - { id: 1, size: 768, alignment: 1 } +body: | + bb.0: + STAfi $img8, %stack.0, 0, implicit-def $a + RTL +... +--- +# CHECK-LABEL: name: near_img +# CHECK: PHA +# CHECK-NEXT: LDA_DP 192 +# CHECK-NEXT: STA_StackRel +# CHECK-NEXT: PLA +name: near_img +tracksRegLiveness: false +stack: + - { id: 0, size: 2, alignment: 1 } + - { id: 1, size: 100, alignment: 1 } +body: | + bb.0: + STAfi $img8, %stack.0, 0, implicit-def $a + RTL +... +--- +# A is already the source, so the far store needs no bracket at all. +# CHECK-LABEL: name: far_acc +# CHECK: STY_DP 250 +# CHECK-NEXT: LDY_Imm16 +# CHECK-NEXT: STA_DPIndLongY 246 +# CHECK-NEXT: LDY_DP 250 +# CHECK-NOT: PLA +name: far_acc +tracksRegLiveness: false +stack: + - { id: 0, size: 2, alignment: 1 } + - { id: 1, size: 768, alignment: 1 } +body: | + bb.0: + STAfi $a, %stack.0, 0, implicit-def $a + RTL +...