diff --git a/demos/rsrcProbe.apl b/demos/rsrcProbe.apl index 1574791..608b345 100644 Binary files a/demos/rsrcProbe.apl and b/demos/rsrcProbe.apl differ diff --git a/runtime/src/libc.c b/runtime/src/libc.c index ccd38c9..5676199 100644 --- a/runtime/src/libc.c +++ b/runtime/src/libc.c @@ -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) { if (!stream) return 0; if (size == 0 || nmemb == 0) return 0; - // size_t is u32 here, so the multiply itself can't overflow. The - // 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; + // size_t is u32 here, so the multiply itself can't overflow. if (stream->kind == FILE_KIND_GSOS) { // Drain unget byte first if present. char *out = (char *)ptr; unsigned long total = (unsigned long)size * (unsigned long)nmemb; unsigned long offset = 0; if (stream->unget >= 0 && total > 0) { - *out++ = (char)stream->unget; + *out = (char)stream->unget; stream->unget = -1; offset = 1; } - if (offset >= total) return offset / size; - __GsosIORecGS r = { - 4, stream->refNum, out, total - offset, 0 - }; - u16 rc = gsosRead(&r); - unsigned long got = offset + r.transferCount; - if (rc != 0 || r.transferCount < total - offset) { - stream->eof = 1; - if (rc != 0 && rc != 0x4C) { stream->err = 1; __gsosLatch(3, rc, stream->refNum, total - offset, r.transferCount); } + // 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 = { + 4, stream->refNum, out + offset, want, 0 + }; + u16 rc = gsosRead(&r); + offset += r.transferCount; + if (rc != 0 || r.transferCount < want) { + stream->eof = 1; + 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; char *out = (char *)ptr; 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) { if (!stream) return 0; if (size == 0 || nmemb == 0) return 0; - // size_t is u32 here, so the multiply itself can't overflow. The - // 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; + // size_t is u32 here, so the multiply itself can't overflow. const char *in = (const char *)ptr; if (stream->kind == FILE_KIND_STDOUT || stream->kind == FILE_KIND_STDERR) { 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->writable) { stream->err = 1; return 0; } unsigned long total = (unsigned long)size * (unsigned long)nmemb; - __GsosIORecGS r = { 4, stream->refNum, (void *)in, total, 0 }; - u16 wrc = gsosWrite(&r); - if (wrc != 0) { - stream->err = 1; - __gsosLatch(4, wrc, stream->refNum, total, r.transferCount); - return (size_t)(r.transferCount / size); + 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); + done += r.transferCount; + if (wrc != 0) { + stream->err = 1; + __gsosLatch(4, wrc, stream->refNum, want, r.transferCount); + break; + } + if (r.transferCount < want) { + __gsosLatch(4, 0, stream->refNum, want, r.transferCount); + break; + } } - if (r.transferCount < total) { - __gsosLatch(4, 0, stream->refNum, total, r.transferCount); - } - 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->writable) { stream->err = 1; return 0; } size_t items = 0; diff --git a/scripts/smokeTest.sh b/scripts/smokeTest.sh index 204e0d5..b884880 100755 --- a/scripts/smokeTest.sh +++ b/scripts/smokeTest.sh @@ -9350,6 +9350,94 @@ EOF rm -rf "$bbDir" 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 +#include +#include +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 # 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 diff --git a/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp b/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp index af8dea9..16f625e 100644 --- a/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp +++ b/src/llvm/lib/Target/W65816/W65816StackSlotCleanup.cpp @@ -956,8 +956,8 @@ 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 the shifts, which the shift-carry idiom - // reads directly. + // listed here. (The one-bit shift pseudos are listed below: + // they model no Defs=[P] either.) 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 @@ -1015,6 +1015,22 @@ bool W65816StackSlotCleanup::runOnMachineFunction(MachineFunction &MF) { // 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 || + // 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::NEGA16 || Opc == W65816::NEGA8 || // Every other load, pseudo or already lowered: a load is never @@ -1051,10 +1067,16 @@ bool W65816StackSlotCleanup::runOnMachineFunction(MachineFunction &MF) { Opc == W65816::CMP_StackRel; }; // 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 - // other entry in isLdaLike touches N/Z alone. + // wrapped even for a BCC/BCS/BVC/BVS: ADDframe (TSC ; CLC ; ADC) and + // the shift pseudos (see isLdaLike). Every other entry in isLdaLike + // touches N/Z alone. 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) { unsigned BrOpc = Br->getOpcode(); diff --git a/src/llvm/test/CodeGen/W65816/shift-flag-corrupt-wrap.mir b/src/llvm/test/CodeGen/W65816/shift-flag-corrupt-wrap.mir new file mode 100644 index 0000000..fd0ca16 --- /dev/null +++ b/src/llvm/test/CodeGen/W65816/shift-flag-corrupt-wrap.mir @@ -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 +...