Fixed capped-segment linking.

This commit is contained in:
Scott Duensing 2026-09-19 18:34:31 -05:00
parent 035c393500
commit 5aaa0b240c
5 changed files with 250 additions and 3 deletions

View file

@ -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++]);

View file

@ -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: {

View file

@ -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

View file

@ -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
...

View file

@ -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
...