More carry flag bugs fixed.

This commit is contained in:
Scott Duensing 2026-10-14 21:17:29 -05:00
parent e139f2b3b8
commit 12de7c8a57
5 changed files with 260 additions and 36 deletions

Binary file not shown.

View file

@ -1700,33 +1700,40 @@ FILE *fopen(const char *path, const char *mode) {
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) { size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) {
if (!stream) return 0; if (!stream) return 0;
if (size == 0 || nmemb == 0) return 0; if (size == 0 || nmemb == 0) return 0;
// size_t is u32 here, so the multiply itself can't overflow. The // size_t is u32 here, so the multiply itself can't overflow.
// 0xFFFE cap is a "single 64KB bank" limit -- the underlying
// mem/GSOS backends address by 16-bit offset, so any single fread
// must fit in one bank.
if (nmemb > (size_t)0xFFFE / size) nmemb = (size_t)0xFFFE / size;
if (stream->kind == FILE_KIND_GSOS) { if (stream->kind == FILE_KIND_GSOS) {
// Drain unget byte first if present. // Drain unget byte first if present.
char *out = (char *)ptr; char *out = (char *)ptr;
unsigned long total = (unsigned long)size * (unsigned long)nmemb; unsigned long total = (unsigned long)size * (unsigned long)nmemb;
unsigned long offset = 0; unsigned long offset = 0;
if (stream->unget >= 0 && total > 0) { if (stream->unget >= 0 && total > 0) {
*out++ = (char)stream->unget; *out = (char)stream->unget;
stream->unget = -1; stream->unget = -1;
offset = 1; offset = 1;
} }
if (offset >= total) return offset / size; // GS/OS Read in pieces of at most 0xFFFE bytes (one bank's worth,
// as the stream layer has always asked for), until the whole
// request is in or a read comes back short: one capped call used
// to return a short count for any request past 64 KB, which a
// caller reads as the end of the file.
while (offset < total) {
unsigned long want = total - offset;
if (want > 0xFFFEul) want = 0xFFFEul;
__GsosIORecGS r = { __GsosIORecGS r = {
4, stream->refNum, out, total - offset, 0 4, stream->refNum, out + offset, want, 0
}; };
u16 rc = gsosRead(&r); u16 rc = gsosRead(&r);
unsigned long got = offset + r.transferCount; offset += r.transferCount;
if (rc != 0 || r.transferCount < total - offset) { if (rc != 0 || r.transferCount < want) {
stream->eof = 1; stream->eof = 1;
if (rc != 0 && rc != 0x4C) { stream->err = 1; __gsosLatch(3, rc, stream->refNum, total - offset, r.transferCount); } if (rc != 0 && rc != 0x4C) { stream->err = 1; __gsosLatch(3, rc, stream->refNum, want, r.transferCount); }
break;
} }
return (size_t)(got / size);
} }
return (size_t)(offset / size);
}
// The memory stream addresses its buffer by 16-bit offset: one bank.
if (nmemb > (size_t)0xFFFE / size) nmemb = (size_t)0xFFFE / size;
if (stream->kind != FILE_KIND_MEM) return 0; if (stream->kind != FILE_KIND_MEM) return 0;
char *out = (char *)ptr; char *out = (char *)ptr;
size_t items = 0; size_t items = 0;
@ -1753,11 +1760,7 @@ size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) {
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) { size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) {
if (!stream) return 0; if (!stream) return 0;
if (size == 0 || nmemb == 0) return 0; if (size == 0 || nmemb == 0) return 0;
// size_t is u32 here, so the multiply itself can't overflow. The // size_t is u32 here, so the multiply itself can't overflow.
// 0xFFFE cap is a "single 64KB bank" limit -- the underlying
// mem/GSOS backends address by 16-bit offset, so any single fwrite
// must fit in one bank.
if (nmemb > (size_t)0xFFFE / size) nmemb = (size_t)0xFFFE / size;
const char *in = (const char *)ptr; const char *in = (const char *)ptr;
if (stream->kind == FILE_KIND_STDOUT || stream->kind == FILE_KIND_STDERR) { if (stream->kind == FILE_KIND_STDOUT || stream->kind == FILE_KIND_STDERR) {
size_t items = 0; size_t items = 0;
@ -1770,18 +1773,29 @@ size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) {
if (stream->kind == FILE_KIND_GSOS) { if (stream->kind == FILE_KIND_GSOS) {
if (!stream->writable) { stream->err = 1; return 0; } if (!stream->writable) { stream->err = 1; return 0; }
unsigned long total = (unsigned long)size * (unsigned long)nmemb; unsigned long total = (unsigned long)size * (unsigned long)nmemb;
__GsosIORecGS r = { 4, stream->refNum, (void *)in, total, 0 }; unsigned long done = 0;
// GS/OS Write in pieces of at most 0xFFFE bytes, until the whole
// request is out or a write comes back short (see fread).
while (done < total) {
unsigned long want = total - done;
if (want > 0xFFFEul) want = 0xFFFEul;
__GsosIORecGS r = { 4, stream->refNum, (void *)(in + done), want, 0 };
u16 wrc = gsosWrite(&r); u16 wrc = gsosWrite(&r);
done += r.transferCount;
if (wrc != 0) { if (wrc != 0) {
stream->err = 1; stream->err = 1;
__gsosLatch(4, wrc, stream->refNum, total, r.transferCount); __gsosLatch(4, wrc, stream->refNum, want, r.transferCount);
return (size_t)(r.transferCount / size); break;
} }
if (r.transferCount < total) { if (r.transferCount < want) {
__gsosLatch(4, 0, stream->refNum, total, r.transferCount); __gsosLatch(4, 0, stream->refNum, want, r.transferCount);
break;
} }
return (size_t)(r.transferCount / size);
} }
return (size_t)(done / size);
}
// The memory stream addresses its buffer by 16-bit offset: one bank.
if (nmemb > (size_t)0xFFFE / size) nmemb = (size_t)0xFFFE / size;
if (stream->kind != FILE_KIND_MEM) return 0; if (stream->kind != FILE_KIND_MEM) return 0;
if (!stream->writable) { stream->err = 1; return 0; } if (!stream->writable) { stream->err = 1; return 0; }
size_t items = 0; size_t items = 0;

View file

@ -9350,6 +9350,94 @@ EOF
rm -rf "$bbDir" rm -rf "$bbDir"
fi fi
# GS/OS fwrite and fread move a request past 64 KB in one call: libc issues
# GS/OS Write / Read in pieces of at most 0xFFFE bytes until the whole
# request is done. It used to cap the request itself at 0xFFFE bytes, so
# one fwrite of 66 KB wrote 65534 bytes and returned short (JoeyLib's
# jlSaveWrite of anything past 64 KB failed). 66 KB puts 2050 bytes in the
# second piece and keeps the run inside runViaFinder's 90 s check (an
# 80 KB one with a multiply per byte did not finish). $70 = 0x55 the
# write returned the full count, $71 = 0x66 the read did and the bytes
# match. Skips under SMOKE_SKIP_GSOSBIGIO=1.
if [ "${SMOKE_SKIP_GSOSBIGIO:-0}" != "1" ] \
&& [ -x "$CLANG" ] && [ -x "$CADIUS" ] && [ -f "$SYSDISK" ] \
&& command -v mame >/dev/null 2>&1; then
log "check: GS/OS fwrite/fread of 66 KB in one call"
bioDir="$(mktemp -d)"
cat > "$bioDir/bigIo.c" <<'EOF'
// One fwrite and one fread of 66 KB to a GS/OS file, from Memory Manager
// blocks. $70 = 0x55 the write returned the full count, $71 = 0x66 the
// read did and the bytes match what was written.
#include <stdio.h>
#include <string.h>
#include <iigs/toolbox.h>
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned long u32;
#define MB ((volatile u8 *)0x70)
#define FILE_LEN 0x10800UL
#define NH_LOCKED 0x8000u
static u8 pat(u32 i) {
return (u8)((u8)i * 7u + (u8)(i >> 8));
}
int main(void) {
u16 id = MMStartUp();
void **hw = (void **)NewHandle(FILE_LEN, id, NH_LOCKED, 0);
void **hr = (void **)NewHandle(FILE_LEN, id, NH_LOCKED, 0);
u8 *wr;
u8 *rd;
u32 i;
FILE *f;
if (!hw || !hr || !*hw || !*hr) {
return 0;
}
wr = (u8 *)*hw;
rd = (u8 *)*hr;
for (i = 0; i < FILE_LEN; i++) {
wr[i] = pat(i);
rd[i] = 0;
}
f = fopen("/DATA/BIGIO", "w");
if (f && fwrite(wr, 1u, FILE_LEN, f) == FILE_LEN && fclose(f) == 0) {
MB[0] = 0x55u;
f = fopen("/DATA/BIGIO", "r");
if (f && fread(rd, 1u, FILE_LEN, f) == FILE_LEN && fclose(f) == 0 &&
memcmp(rd, wr, FILE_LEN) == 0) {
MB[1] = 0x66u;
}
}
return 0;
}
EOF
"$CLANG" --target=w65816 -I"$PROJECT_ROOT/runtime/include" -O2 -ffunction-sections \
-c "$bioDir/bigIo.c" -o "$bioDir/bigIo.o"
"$PROJECT_ROOT/tools/link816" -o "$bioDir/bigIo.bin" --text-base 0x1000 --full-bank \
--map "$bioDir/bigIo.map" --reloc-out "$bioDir/bigIo.reloc" \
"$PROJECT_ROOT/runtime/crt0Gsos.o" "$bioDir/bigIo.o" \
"$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 66 KB I/O smoke: link failed"
"$PROJECT_ROOT/tools/omfEmit" --input "$bioDir/bigIo.bin" --map "$bioDir/bigIo.map" \
--base 0x1000 --entry __start --output "$bioDir/bigIo.omf" \
--name HELLO --expressload --relocs "$bioDir/bigIo.reloc" >/dev/null 2>&1
[ -s "$bioDir/bigIo.omf" ] || die "GS/OS 66 KB I/O smoke: omfEmit produced no OMF"
if ! bash "$PROJECT_ROOT/scripts/runViaFinder.sh" "$bioDir/bigIo.omf" \
--check 0x70=0x55 0x71=0x66 >/dev/null 2>&1; then
die "GS/OS 66 KB I/O smoke FAILED (\$70 fwrite count, \$71 fread count + bytes)"
fi
rm -rf "$bioDir"
fi
# GS/OS program exit: returning from main is exit(main's value), which must # 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 # 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 # QUIT back to the Finder. crt0Gsos used to issue QUIT with the pointer

View file

@ -956,8 +956,8 @@ bool W65816StackSlotCleanup::runOnMachineFunction(MachineFunction &MF) {
// real CMP. NOTE: the *fi variants (ANDfi/ORAfi/EORfi) and // real CMP. NOTE: the *fi variants (ANDfi/ORAfi/EORfi) and
// ADC/SBC DO model Defs=[P] and can legitimately be the branch's // 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 // flag source, so they are handled by $p liveness and are NOT
// listed here; same for the shifts, which the shift-carry idiom // listed here. (The one-bit shift pseudos are listed below:
// reads directly. // they model no Defs=[P] either.)
Opc == W65816::EORi16imm || Opc == W65816::EORabs || Opc == W65816::EORi16imm || Opc == W65816::EORabs ||
// INC/DEC in every form. None of them models Defs=[P] either, // 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 // so at this pass's point no branch can take its flags from
@ -1015,6 +1015,22 @@ bool W65816StackSlotCleanup::runOnMachineFunction(MachineFunction &MF) {
// the BNE read the last AND's Z, so the break never fired // the BNE read the last AND's Z, so the break never fired
// (JoeyLib's IIgs serial-loss probe, 2026-09-28). // (JoeyLib's IIgs serial-loss probe, 2026-09-28).
Opc == W65816::SRL8A || Opc == W65816::SHL8A || Opc == W65816::SRL8A || Opc == W65816::SHL8A ||
// The shift pseudos, which lower in the AsmPrinter to ASL A /
// LSR A (ASRA16: CMP #$8000 ; ROR A; the *15A forms: a shift,
// LDA #0 and a rotate). They rewrite N, Z and C, model no
// Defs=[P], and are cheap, so Machine CSE's PRE hoists one to
// the end of a dominating block -- between its CMP and the
// terminator. Live symptom: `for (row = 0; row < h; row++)`
// after `dstByteStart = dx >> 1` lowered to
// `cmp #$8001 ; lda n,s ; lsr a ; sta m,s ; bcc exit`; the BCC
// read bit 0 of dx, so every even-x interpreted sprite draw
// skipped its rows (JoeyLib's IIgs sprite interpreter,
// 2026-10-13). They are carry corrupters too (below).
Opc == W65816::ASLA16 || Opc == W65816::LSRA16 ||
Opc == W65816::ASLA8 || Opc == W65816::LSRA8 ||
Opc == W65816::ASRA16 ||
Opc == W65816::SRL15A || Opc == W65816::SHL15A ||
Opc == W65816::SRA15A ||
Opc == W65816::XBA || Opc == W65816::XBA16 || Opc == W65816::XBA || Opc == W65816::XBA16 ||
Opc == W65816::NEGA16 || Opc == W65816::NEGA8 || Opc == W65816::NEGA16 || Opc == W65816::NEGA8 ||
// Every other load, pseudo or already lowered: a load is never // Every other load, pseudo or already lowered: a load is never
@ -1051,10 +1067,16 @@ bool W65816StackSlotCleanup::runOnMachineFunction(MachineFunction &MF) {
Opc == W65816::CMP_StackRel; Opc == W65816::CMP_StackRel;
}; };
// The subset of corrupters that also rewrite C/V, and so must be // The subset of corrupters that also rewrite C/V, and so must be
// wrapped even for a BCC/BCS/BVC/BVS. Only ADDframe today: every // wrapped even for a BCC/BCS/BVC/BVS: ADDframe (TSC ; CLC ; ADC) and
// other entry in isLdaLike touches N/Z alone. // the shift pseudos (see isLdaLike). Every other entry in isLdaLike
// touches N/Z alone.
auto isCarryCorrupting = [](unsigned Opc) { auto isCarryCorrupting = [](unsigned Opc) {
return Opc == W65816::ADDframe; return Opc == W65816::ADDframe ||
Opc == W65816::ASLA16 || Opc == W65816::LSRA16 ||
Opc == W65816::ASLA8 || Opc == W65816::LSRA8 ||
Opc == W65816::ASRA16 ||
Opc == W65816::SRL15A || Opc == W65816::SHL15A ||
Opc == W65816::SRA15A;
}; };
for (MachineInstr *Br : Branches) { for (MachineInstr *Br : Branches) {
unsigned BrOpc = Br->getOpcode(); unsigned BrOpc = Br->getOpcode();

View file

@ -0,0 +1,100 @@
# Pin: the shift pseudos (ASLA16 / LSRA16 / ASLA8 / LSRA8 / ASRA16 and the
# *15A forms) count as flag CORRUPTION -- of N/Z and of C -- in the PHP/PLP
# wrap (W65816StackSlotCleanup Pass -2.5), not as the flag-defining test.
#
# They lower in the AsmPrinter to ASL A / LSR A (ASRA16: CMP #$8000 ; ROR A;
# the *15A forms: a shift, LDA #0 and a rotate), which rewrite N, Z and C,
# but model no Defs=[P]. Being cheap, Machine CSE's PRE hoisted one to the
# end of a dominating block -- between its CMP and the terminator -- and
# isFlagDefining's default then took the shift for THE test: the walk
# stopped there and the pass declined to wrap.
#
# Live symptom (JoeyLib's IIgs sprite interpreter, 2026-10-13): a loop guard
# `row < h` after `dstByteStart = dx >> 1` lowered to
# cmp #$8001 ; lda n,s ; lsr a ; sta m,s ; bcc exit
# so the BCC read bit 0 of dx and every even-x draw skipped its rows.
#
# lsra16_carry: the reported shape under a BCC. The reload in front of the
# shift only corrupts N/Z, harmless to a carry branch, so the
# wrap covers the shift alone; the spill after it stays outside.
# asla16_nz: an ASLA16 under a BNE: the reload corrupts Z too, so the wrap
# starts at it.
#
# Each case reads %stack.2 after the branch so its store survives the pass's
# never-read slot sweep, and the branch targets take X live in, so the wrap
# cannot be replaced by the TAX ... CPX #k form (cmp-via-x.mir).
#
# RUN: llc -mtriple=w65816 -run-pass=w65816-stack-slot-cleanup -o - %s | FileCheck %s
--- |
target triple = "w65816"
define void @lsra16_carry() { ret void }
define void @asla16_nz() { ret void }
...
---
# CHECK-LABEL: name: lsra16_carry
# CHECK: CMPi16imm killed $a, 32769, implicit-def $p
# CHECK-NEXT: $a = LDAfi %stack.1, 0
# CHECK-NEXT: PHP
# CHECK-NEXT: $a = LSRA16 killed $a
# CHECK-NEXT: PLP
# CHECK-NEXT: STAfi killed $a, %stack.2, 0
# CHECK-NEXT: BCC %bb.1, implicit $p
name: lsra16_carry
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, 32769, implicit-def $p
$a = LDAfi %stack.1, 0
$a = LSRA16 killed $a
STAfi killed $a, %stack.2, 0, implicit-def $a
BCC %bb.1, implicit $p
BRA %bb.2
bb.1:
liveins: $x
$a = LDAfi %stack.2, 0
RTL
bb.2:
liveins: $x
$a = LDAfi %stack.2, 0
RTL
...
---
# CHECK-LABEL: name: asla16_nz
# CHECK: CMPi16imm killed $a, 3, implicit-def $p
# CHECK-NEXT: PHP
# CHECK-NEXT: $a = LDAfi %stack.1, 0
# CHECK-NEXT: $a = ASLA16 killed $a
# CHECK-NEXT: PLP
# CHECK-NEXT: STAfi killed $a, %stack.2, 0
# CHECK-NEXT: BNE %bb.1, implicit $p
name: asla16_nz
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 = ASLA16 killed $a
STAfi killed $a, %stack.2, 0, implicit-def $a
BNE %bb.1, implicit $p
BRA %bb.2
bb.1:
liveins: $x
$a = LDAfi %stack.2, 0
RTL
bb.2:
liveins: $x
$a = LDAfi %stack.2, 0
RTL
...