From 91629dc5eb8d6a70c38af6f311f8f82a17acaac4 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 25 Aug 2026 16:43:41 -0500 Subject: [PATCH] Input and serial fixes. --- LLVM816-ASKS.md | 232 ++++++++++++++++++++++++++++++++++++ include/joey/input.h | 6 +- include/joey/platform.h | 4 + include/joey/serial.h | 12 ++ src/core/input.c | 4 +- src/core/port.h | 10 ++ src/core/serial.c | 8 ++ src/dos/input.c | 9 +- src/dos/serial.c | 17 +++ src/generic/genericSerial.c | 7 ++ src/iigs/serial.c | 24 +++- tools/xdftool.py | 124 ++++++++++++------- 12 files changed, 408 insertions(+), 49 deletions(-) diff --git a/LLVM816-ASKS.md b/LLVM816-ASKS.md index f7e885d..7c15e12 100644 --- a/LLVM816-ASKS.md +++ b/LLVM816-ASKS.md @@ -213,3 +213,235 @@ and #$ff`. ABI verified from a compiled probe: pointer lo16 in A, bank in X, return in A, $e0-$e3 imaginary-register scratch. Any OTHER volatile I/O read through a variable pointer (input/video HALs, future code) is still exposed until the codegen fix lands. + +## 8. Peeled-first-iteration address negation is one's complement, not two's (2026-08-20) -- FIXED IN TOOLCHAIN same day (see end of item) + +A loop that indexes a local array at a base computed by SUBTRACTING a +uint8_t parameter (`k = 8u - nb;` then `t[(uint8_t)(k + i)]` inside a +counted loop) is compiled with the FIRST iteration peeled, and that +peeled iteration's address is computed as + + base + sign_extend(~nb) ; one byte LOW: ~nb = -nb - 1 + +where two's complement (`-nb`) was needed. Every later iteration +increments correctly, so exactly the first element is read one byte +low and the remaining seven are right -- a silent wrong-VALUES class +with no crash, in the same derived-induction-variable family as the +modulo-advance item. + +The emitted idiom (RetroNet src/crypto/rnSipHash.c sipRot, w65816 +-O2 -ffreestanding -ffunction-sections, disassembly of the peeled +first read's address): + + lda 0xb6,s ; nb + eor #0x1aff ; low byte: nb ^ 0xff = ~nb (high byte junk) + and #0xff + eor #0x80 ; sign-extend byte + clc + adc #0xff80 ; (x ^ 0x80) - 0x80 + ... + tsc + clc + adc #0xa3 ; t[] frame base (t + 8 region) + clc + adc 0x1f,s ; += sext(~nb) --> t + 8 - nb - 1 = t[k-1] + +Blast radius measured before the workaround: EVERY SipHash-2-4 tag the +IIgs client computed was wrong (the rotate primitive feeds every round), +so every sealed uplink failed server-side authentication. Nothing +client-side ever caught it because the protocol's selective encryption +leaves downlink frames plaintext -- silent until a peer verifies. +Root-caused via on-target known-answer tests + a per-primitive state +probe (SIPBLOCK) diffed against the host build of the same source; the +"first iteration reads index k-1" model reproduces both on-target wrong +tags bit-for-bit end to end. + +WORKAROUND LANDED IN RETRONET: sipRot now takes k (the precomputed read +base) instead of nb, removing the 8-nb negation from the function; the +emitted first-iteration address becomes a plain base + k and all KATs +pass on target. Any other loop whose peeled first iteration derives +its address from a subtracted narrow parameter is still exposed. + +RESOLUTION (2026-08-20): root cause was NOT the address lowering -- the +IR and ISel were correct (a true NEGA8 pseudo). The AsmPrinter expanded +NEGA8 as an UNWRAPPED `EOR #$FF ; INA` on a false "already 8-bit M" +assumption: in the real 16-bit-M context the CPU decoded the 2-byte +EOR #imm8 as 3-byte EOR #imm16 and swallowed the INA opcode ($1A) as the +immediate's high byte -- the disassembly tell is `eor #$1AFF`. FIXED in +W65816AsmPrinter.cpp: NEGA8 now emits emitSepM/emitRepM around the pair, +exactly like LDA8absX/STA8absX (safe: SepRepCleanup's isMNeutral never +extends an M=8 window across the pseudo). Audited: NEGA8 was the only +unwrapped Imm8 expansion. Blast radius before the fix: 6 sites in the +RetroNet IIgs client -- the SipHash rotate (every sealed uplink failed to +authenticate) and 5 rnWidget functions (index corruption; wild-jump BRK +during the download offer frame; likely the phantom-FORM garbage too). + +Original ask (kept for the repro): compile the pre-workaround sipRot +shape below at -O2 for w65816 and inspect the first read's address chain. +(Verbatim pre-workaround function; gSipV is a 40-byte global.) + +```c +static void sipRot(uint8_t a, uint8_t nb, uint8_t mode) { + uint8_t t[16]; + uint8_t i; + uint8_t k; + uint8_t x; + + for (i = 0u; i < 8u; i++) { + x = gSipV[(uint8_t)(a + i)]; + t[i] = x; + t[(uint8_t)(i + 8u)] = x; + } + k = (uint8_t)(8u - nb); + for (i = 0u; i < 8u; i++) { + x = t[(uint8_t)(k + i)]; + if (mode == 3u) { + x = (uint8_t)((x >> 3) | (uint8_t)(t[(uint8_t)(k + i + 1u)] << 5)); + } else if (mode == 1u) { + x = (uint8_t)((x << 1) | (uint8_t)(t[(uint8_t)(k + i - 1u)] >> 7)); + } + gSipV[(uint8_t)(a + i)] = x; + } +} +``` + +## 9. i8 store to a 1-byte stack slot is a 16-bit STA d,s that overflows the return address (2026-08-20) -- FIXED IN TOOLCHAIN same day + +M=16 is the default mode, so `sta d,s` writes TWO bytes. When the target +frame object is only 1 byte (an i8 spill / i8 arg-save) at the TOP slot of a +frame, the second byte lands on the caller's saved RETURN ADDRESS low byte and +the callee's RTL jumps mid-instruction into a different function. Live proof: +RetroNet's rnClientOnFileRecord saving its uint8_t `op` clobbered its own +return 0x80->0x01, so its RTL landed inside a different rasterizer switch case, +skipped the caller's arg cleanup, and left the DISPLAY record-walk's stack +pointer 4 low -> $F6 (big-frame pointer) wrong -> the walk byte-crawled forever +on the offer frame (the IIgs download "offer-silence": FILE_OFFER never +processed, client never drained the serial). + +Minimal repro: `void fwd(uint8_t op, const uint8_t *r){ sink(op, r); }` at -O2 +for w65816 emits `sta 0x2, s` (16-bit) to save op adjacent to the return. + +FIX (W65816RegisterInfo.cpp eliminateFrameIndex, STAfi case): when +`MFI.getObjectSize(FI) == 1`, wrap the STA_StackRel in SEP #$20 / REP #$20 so a +1-byte slot gets a genuine 8-bit store. Only the STORE is a hazard; a 16-bit +LOAD of the extra byte is a harmless read (the high byte of an i8 value is +ignored), so LDAfi is left 16-bit. Verified: fwd now emits +`sep #$20 ; sta 0x1,s ; rep #$20`. Same defect family as ASKS #8 (NEGA8) and +the $F8 far-frame bank byte -- "an 8-bit operation emitted without the mode +wrap in default-16-bit-M code." + +## 10. Pointer-deref to a GLOBAL hardcodes bank 0 (STZ $E2) under the GS/OS Loader (2026-08-20) -- FIXED IN TOOLCHAIN 2026-08-21 (provenance-based; see end) + +The LDAptr / STAptr / STBptr custom inserter (W65816ISelLowering.cpp) lowers a +[dp],Y indirect-long deref with the BANK byte forced to 0 via `STZ $E2` (unless +the LoaderBankDeref path, which uses $BE, is taken). Bank-0 is correct for a +pointer to a STACK local (the 65816 stack is always bank 0) but WRONG for a +pointer to a GLOBAL: under the GS/OS Loader the program's BSS/globals are placed +in the program bank ($BE, e.g. $0A), not bank 0. So `&someGlobal` dereferenced +through this path reads bank-0 garbage, while the SAME global accessed directly +via `sta/lda abs` (DBR-relative, DBR=$BE) is correct -- a silent, layout- +dependent miscompile. The inserter's own comment already flags this tension +(gmtime's __gmtimeBuf hit it). + +Live proof: RetroNet's inlined rnXferRequest materialised `&gX.id` (a static +global) and read it through LDAptr; the FILE_REQUEST then carried xferId=0xAD +(bank-0 garbage) instead of 1, and the server rejected the download. 0xAD is +not any valid gX field -- the tell of a wrong-bank read. Disasm: +`lda # ; sta $e0 ; stz $e2 ; lda [$e0],y`. + +WORKAROUND in app: `noinline` on the function so the global is read in the +caller through DBR-relative abs16 and passed by the ABI (same shape as the +existing rnXferState workaround). + +Ask: the deref bank cannot be a blanket 0 or blanket $BE when stack pointers +(bank 0) and global pointers ($BE) are both derefed through one pseudo. The +principled fix is a 24-bit pointer representation (carry the bank in the pointer +value) or provenance tracking so a global-derived pointer derefs with $BE and a +stack-derived pointer with 0. + +Fixes EVALUATED and rejected from the app side (2026-08-20): + 1. -w65816-loader-bank-deref (existing flag): whole-program, flips EVERY + LDAptr to $BE. Fixes global derefs but breaks stack-pointer derefs (the + reason LDAptrBank0 exists for va_arg). Also the runtime *.o are shared + across Loader AND non-Loader (GNO/smoke) builds, so it can't be enabled + for just the S16/Loader target without a per-mode runtime split. + 2. Fold (load ) -> abs16+DBR at selection -- the + clean targeted fix, but NOT minimally reproducible: every small repro + (inlined forwarder, barrier-forced re-read, struct field) correctly + selects abs16. The mis-selection only appears inside a very large inlined + function (RetroNet rnFileRecord, ~0x916 bytes, three transfer routines + inlined), so it is a scheduling/CSE artifact of address materialization + under register pressure, not a stable pattern -- hard to fix safely + without provenance. +RESOLUTION (2026-08-21, W65816ISelLowering.cpp LDAptr/STAptr/STBptr custom +inserter): instead of a blanket bank policy, use PROVENANCE. When the pointer +being derefed traces (through COPYs) to an LDAi16imm of a global / external +symbol, the deref is exactly a DBR-relative absolute access to that global -- +the same thing a direct `global` reference compiles to -- so emit LDAabs / +STAabs / STA8abs with the global operand and skip the [dp],Y deref entirely. +That lands in DBR's bank (correct for a global) with no bank guess, and only +GLOBAL-address derefs are touched -- stack pointers (bank 0) and heap/other +pointers keep the existing deref path, so no stack-out-param regression. NB the +bank canNOT be sourced from $BE: $BE is the code segment's PBR and in a +multi-segment program the data (BSS) segment is a different bank; only DBR (via +abs) is right. Single-segment builds are unaffected (DBR==0==old STZ bank). +Verified: RetroNet's rnXferRequest/rnXferState no longer need `noinline` (both +removed) and the inlined rnFileRecord emits zero bank-0 pointer-derefs; the IIgs +download DISK-VERIFIES from the pure-clean build. Also fixes the runtime gmtime +__gmtimeBuf case this item's parent note describes. What is still NOT covered: +a global address that flows through pointer ARITHMETIC the trace can't follow +(e.g. `p = &g; p += i; *p`) -- there the provenance is lost and the deref falls +back to bank 0; those remain candidates for the 24-bit-pointer rework. + +## 11. ADDframe (stack-slot LEA) scheduled between a CMP and its branch clobbers the flags (2026-08-24) -- FIXED IN TOOLCHAIN same day + +`ADDframe` - the LEA-equivalent that puts the address of a stack slot in A - +expands at PEI (W65816RegisterInfo.cpp eliminateFrameIndex) to `TSC; CLC; +ADC #disp`. That sequence rewrites N/Z (TSC, ADC), C (CLC, ADC) and V (ADC). +The pseudo deliberately carries no `Defs = [P]` (the same reasoning as the +LDA pseudos, see the NOTE above LDAi16imm in W65816InstrInfo.td) and is +`isReMaterializable`, so a post-RA pass may legally place it between a CMP +and the Bxx that consumes the CMP's flags. Machine Copy Propagation did +exactly that in RetroNet's src/platform/rnClient.c (main, the widget action +dispatch): + + lda 0xf,s ; act.kind + cmp #0x3 ; == RN_WA_SUBMIT ? + tsc ; \ + clc ; > ADDframe: &act, hoisted here for the SUBMIT arm + adc #0xc1 ; / + bne .LBB3_61 ; tests (SP+0xC1) != 0 - ALWAYS taken + +so `act.kind == RN_WA_SUBMIT` could never be true: the LOGIN button +activated, rnWidgetActivate wrote kind=3, and rnClientOnSubmit was never +reached. On the IIgs the login form was never submitted; the harness's +Tab/Return retry loop then produced exactly nine no-widget Return uplinks, +which is what pinned it. A scan of every TU in the IIgs client found this +ONE instance (`cmp` immediately followed by `tsc` before the branch), which +is why only the submit was affected. + +The existing mitigation - the PHP/PLP wrap in W65816StackSlotCleanup.cpp +Pass -2.5 - did not catch it for two reasons: (1) ADDframe was not in the +pass's corrupting set (isLdaLike), so the backward walk from the branch hit +it, took isFlagDefining's default ("anything else defines flags") and called +it the TEST, then declined to wrap; (2) the pass only collects BEQ/BNE/BMI/ +BPL, on the (true) grounds that LDA-like ops leave C alone - but ADDframe +also clobbers C and V, so a `cmp; ADDframe; bcc` would have been miscompiled +with no protection at all. + +RESOLUTION (2026-08-24, W65816StackSlotCleanup.cpp Pass -2.5): + * ADDframe is now in isLdaLike (N/Z-corrupting), so the walk continues + past it to the real CMP and wraps it: `cmp #3; php; tsc; clc; adc #disp+1; + plp; bne`. It was already in isStackRel and the bump list, so its + ImmOffset gets the +1 that compensates PHP's S decrement; the FP-relative + exemption in the bump is skipped for ADDframe because it has no FP- + relative expansion (always TSC+ADC, i.e. always SP-relative). + * BCC/BCS/BVC/BVS are collected as well, with a separate + isCarryCorrupting predicate (ADDframe only): under a C/V branch an + N/Z-only corrupter is walked past without a wrap, so the size of every + existing carry-branch sequence is unchanged. + * Pinned by llvm/test/CodeGen/W65816/addframe-flag-wrap.mir (nz, carry, + and the lda-under-carry no-wrap case). +Verified: the rnClient.c TU rescans clean (zero cmp/tsc/branch sequences in +all 20 IIgs client TUs) and the IIgs login submits live - see the RetroNet +e2e matrix (`joey-iigs session`). diff --git a/include/joey/input.h b/include/joey/input.h index 55df6f2..e985608 100644 --- a/include/joey/input.h +++ b/include/joey/input.h @@ -104,6 +104,10 @@ typedef enum { #define JL_CHAR_TAB 0x09 #define JL_CHAR_RETURN 0x0D #define JL_CHAR_ESCAPE 0x1B +// The FORWARD delete (the key labelled Del/Delete on a PC, ST or Amiga keyboard - not backspace). A text +// editor needs both: backspace removes the character BEFORE the caret, this one removes the character +// UNDER it. 0x7F is ASCII DEL, which is what every one of these keyboards already reports. +#define JL_CHAR_DELETE 0x7F // Typed-character queue capacity (ring buffer; one slot stays empty, // so JL_CHAR_QUEUE_SIZE - 1 characters can be pending). Must be a @@ -114,7 +118,7 @@ void jlInputPoll(void); // Pop the next typed character as a 7-bit code, or -1 if the queue // is empty. Codes: printable ASCII 0x20..0x7E, plus JL_CHAR_BACKSPACE, -// JL_CHAR_TAB, JL_CHAR_RETURN, JL_CHAR_ESCAPE. Shift, caps lock, and +// JL_CHAR_TAB, JL_CHAR_RETURN, JL_CHAR_ESCAPE, JL_CHAR_DELETE. Shift, caps lock, and // the keyboard layout are already applied by the backend, so // punctuation and shifted symbols arrive correctly on every port. // (The layout is the machine's own on IIgs/Amiga/ST/X68000; the DOS diff --git a/include/joey/platform.h b/include/joey/platform.h index 088d8d5..eadd446 100644 --- a/include/joey/platform.h +++ b/include/joey/platform.h @@ -167,6 +167,8 @@ #define JL_HAS_SERIAL_WRITE #define JL_HAS_SERIAL_AVAILABLE #define JL_HAS_SERIAL_FLUSH + // Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts). + #define JL_HAS_SERIAL_SET_RTS // save files + disk space: delete (GS/OS Destroy), dir-ensure (GS/OS Create // $2001, storageType 13), disk-free (GS/OS Volume $2008 via prefix 0). #define JL_HAS_SAVE_DIR_ENSURE @@ -353,6 +355,8 @@ #define JL_HAS_SERIAL_WRITE #define JL_HAS_SERIAL_AVAILABLE #define JL_HAS_SERIAL_FLUSH + // Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts). + #define JL_HAS_SERIAL_SET_RTS // save files + disk space (POSIX mkdir/remove + int21h/36h disk-free) #define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DELETE diff --git a/include/joey/serial.h b/include/joey/serial.h index 71b055a..b9224ad 100644 --- a/include/joey/serial.h +++ b/include/joey/serial.h @@ -101,4 +101,16 @@ int16_t jlSerialReadByte(void); // Discard any buffered received bytes. void jlSerialFlush(void); +// ASSERT OR DROP RTS, for a caller that does its own receive-side flow control. +// +// JL_SERIAL_FLOW_RTSCTS asks the PORT to handshake; this is the other half - the application saying +// "my buffer is nearly full, stop sending" and later "go ahead", which is what a client with a small +// ring and a peer that can be told to pause actually needs. RetroNet's 6551 clients (C64, Apple II) +// have done exactly this for a long time and the 16-bit tier could not, so a JoeyLib target's only +// defence against an overrun was the packet layer's retransmit. +// +// `on` = true asserts RTS (ready to receive). A port with no controllable RTS line no-ops, and a +// closed port no-ops; neither is an error, so a caller can drive it unconditionally. +void jlSerialSetRts(bool on); + #endif diff --git a/src/core/input.c b/src/core/input.c index 69b4a5d..db81cad 100644 --- a/src/core/input.c +++ b/src/core/input.c @@ -69,7 +69,9 @@ void jlInputCharPush(uint8_t ch) { uint8_t head; uint8_t next; - if (ch > 0x7E) { + // 0x7F is the FORWARD delete, not junk: it is the one code above the printable range the queue + // carries, because a text editor needs a key that removes the character under the caret. + if (ch > 0x7E && ch != JL_CHAR_DELETE) { return; } if (ch < 0x20 && diff --git a/src/core/port.h b/src/core/port.h index 177c6de..b18a9b7 100644 --- a/src/core/port.h +++ b/src/core/port.h @@ -1011,6 +1011,16 @@ void jlpGenericSerialFlush(void); #define jlpSerialFlush() jlpGenericSerialFlush() #endif #endif +// Application-driven RTS (jlSerialSetRts). The generic stub does nothing, which is the honest answer +// for a port whose RTS line is not controllable - a caller drives this unconditionally. +void jlpGenericSerialSetRts(bool on); +#if !defined(jlpSerialSetRts) + #if defined(JL_HAS_SERIAL_SET_RTS) + void jlpSerialSetRts(bool on); + #else + #define jlpSerialSetRts(_o) jlpGenericSerialSetRts((_o)) + #endif +#endif // --- Save files + disk space (platform-only; blank-port stub generic + JL_HAS override) --- diff --git a/src/core/serial.c b/src/core/serial.c index 6059f1d..b024307 100644 --- a/src/core/serial.c +++ b/src/core/serial.c @@ -109,6 +109,14 @@ int16_t jlSerialReadByte(void) { } +void jlSerialSetRts(bool on) { + if (!gSerialOpen) { + return; + } + jlpSerialSetRts(on); +} + + uint16_t jlSerialWrite(const uint8_t *buf, uint16_t len) { if (!gSerialOpen || buf == NULL || len == 0u) { return 0u; diff --git a/src/dos/input.c b/src/dos/input.c index e089eee..49e397f 100644 --- a/src/dos/input.c +++ b/src/dos/input.c @@ -93,6 +93,7 @@ // (0xE0 0x1C) and keypad '/' (0xE0 0x35). #define SCAN_ENTER 0x1C #define SCAN_SLASH 0x35 +#define SCAN_DELETE 0x53 // shared by keypad '.' and the E0-prefixed Delete key // Private ISR->poll ring for translated characters. Power of two so // the wrap is a mask; holds ISR_CHAR_QUEUE_SIZE - 1 pending bytes. @@ -351,10 +352,14 @@ static void keyboardIsr(void) { // commands, not text, so they produce no character. ch = 0; if (extended) { - // Of the 0xE0 pairs only keypad Enter and keypad '/' - // type; the rest (arrows, Home/End, ...) do not. + // Of the 0xE0 pairs only keypad Enter, keypad '/' and + // DELETE type; the rest (arrows, Home/End, ...) do not. + // Delete is the FORWARD delete a text editor needs - + // backspace already types 0x08 from the main block. if (code == SCAN_ENTER || code == SCAN_SLASH) { ch = gScanAsciiNormal[code]; + } else if (code == SCAN_DELETE) { + ch = JL_CHAR_DELETE; } } else if (code >= SCAN_KEYPAD_FIRST && code <= SCAN_KEYPAD_LAST && gIsrNumLock && gKeypadAscii[code - SCAN_KEYPAD_FIRST] != 0) { diff --git a/src/dos/serial.c b/src/dos/serial.c index 42e119e..b1ab6a4 100644 --- a/src/dos/serial.c +++ b/src/dos/serial.c @@ -278,6 +278,23 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { } +// RTS on demand (jlSerialSetRts): MCR bit 1. DTR and OUT2 are preserved - OUT2 physically gates this +// UART's interrupt to the PIC, so clearing it while dropping RTS would silence the receiver entirely, +// which is the opposite of what a flow-control pause is for. +void jlpSerialSetRts(bool on) { + uint8_t mcr; + + if (gComBase == 0u) { + return; + } + mcr = (uint8_t)(MCR_DTR | MCR_OUT2); + if (on) { + mcr = (uint8_t)(mcr | MCR_RTS); + } + outportb((int)(gComBase + UART_MCR), mcr); +} + + void jlpSerialPoll(void) { // The RX IRQ fills the ring in the background; nothing to pump. } diff --git a/src/generic/genericSerial.c b/src/generic/genericSerial.c index f06866e..ba9f6d0 100644 --- a/src/generic/genericSerial.c +++ b/src/generic/genericSerial.c @@ -34,6 +34,13 @@ void jlpGenericSerialPoll(void) { } +// A port with no controllable RTS line: doing nothing is the honest answer, and it lets a caller drive +// jlSerialSetRts unconditionally instead of guarding every call site with a platform test. +void jlpGenericSerialSetRts(bool on) { + (void)on; +} + + uint16_t jlpGenericSerialRead(uint8_t *buf, uint16_t max) { (void)buf; (void)max; diff --git a/src/iigs/serial.c b/src/iigs/serial.c index de5f9af..09ab3e6 100644 --- a/src/iigs/serial.c +++ b/src/iigs/serial.c @@ -95,6 +95,10 @@ static uint16_t gRxTail; static bool gOpen; static bool gIsScc; // true: 8530 SCC; false: 6551 ACIA static volatile uint8_t *gStatus; // SCC command reg (RR0) / 6551 status reg +static uint8_t gWr5; // the SCC's live WR5 (Tx bits + DTR + RTS), so jlpSerialSetRts + // can flip ONE bit without guessing the rest back: WR5 is + // write-only on this chip, and rebuilding it from scratch would + // silently drop the data-bit and Tx-enable settings static volatile uint8_t *gData; // data register (both) @@ -219,7 +223,8 @@ static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, uint8_ sccWrite(ctrl, 13u, (uint8_t)((tc >> 8) & 0xFFu)); // time constant high sccWrite(ctrl, 14u, 0x03u); // BRG enable, source = PCLK sccWrite(ctrl, 3u, (uint8_t)(wr3 | 0x01u)); // Rx enable - sccWrite(ctrl, 5u, (uint8_t)(wr5 | 0x8Au)); // Tx enable + DTR + RTS + gWr5 = (uint8_t)(wr5 | 0x8Au); + sccWrite(ctrl, 5u, gWr5); // Tx enable + DTR + RTS } @@ -352,6 +357,23 @@ void iigsSerialPump(void) { } +// RTS on demand (jlSerialSetRts). SCC: WR5 bit 1, applied to the retained copy so the Tx enable, DTR and +// data-bit fields survive. 6551: the command register's bits 3:2 - %10 asserts RTS with the receiver +// interrupt disabled (this HAL polls), %00 deasserts it AND gates the transmitter off, which is why the +// deassert is only ever a pause. +void jlpSerialSetRts(bool on) { + if (!gOpen) { + return; + } + if (gIsScc) { + gWr5 = on ? (uint8_t)(gWr5 | 0x02u) : (uint8_t)(gWr5 & (uint8_t)~0x02u); + sccWrite(gStatus, 5u, gWr5); + return; + } + gStatus[1] = on ? 0x09u : 0x01u; // 6551 command: DTR on, RTS asserted / deasserted +} + + void jlpSerialPoll(void) { uint16_t tail; uint16_t guard; diff --git a/tools/xdftool.py b/tools/xdftool.py index 6a9ad30..44e7aee 100755 --- a/tools/xdftool.py +++ b/tools/xdftool.py @@ -101,8 +101,12 @@ class Xdf: continue name = e[0:8].decode("ascii", "replace").rstrip() ext = e[8:11].decode("ascii", "replace").rstrip() + # Human68k EXTENDED names: chars 9-18 of the stem live in bytes 12-21 (see encode_name_ext). + # Fold them back in so a listing shows the full name a guest wrote (e.g. the 9-char blob keys). + more = e[12:22].split(b"\x00", 1)[0].decode("ascii", "replace").rstrip() + stem = name + more out.append({ - "name": f"{name}.{ext}" if ext else name, + "name": f"{stem}.{ext}" if ext else stem, "cluster": struct.unpack(" self.max_cluster: + raise OSError(f"{self.path}: directory cluster chain loops") + # Every slot is used: append a fresh, all-free cluster and write into its first slot. + free = self.free_clusters() + if not free: + raise OSError(f"{self.path}: no free cluster to extend directory") + newc = free[0] + self.fat_set(last, newc) + self.fat_set(newc, 0xFFF) + base = self.cluster_offset(newc) + self.data[base:base + self.cluster_bytes] = bytes(self.cluster_bytes) + self.data[base:base + DIR_ENTRY_SIZE] = entry + + def add_file(self, name, payload): + self.delete(name) # overwrite semantics + # encode_name_ext writes the Human68k EXTENDED name (a strict superset of 8.3: ext10 is all-NUL + # for a plain 8.3 name), so names up to 18 chars - like RetroNet's 9-char blob keys - land in a + # form the guest and xdftool's own find()/read_file() both resolve. + main11, ext10 = self.encode_name_ext(name) + head = self._alloc_write_payload(payload) slot = None - for i, off, e in self._entries(): + for _, off, e in self._entries(): if e[0] in (FREE_MARKER, DELETED_MARKER): slot = off break if slot is None: raise OSError(f"{self.path}: root directory full ({self.root_entries} entries)") + self.data[slot:slot + DIR_ENTRY_SIZE] = self._dir_entry( + main11, ATTR_ARCHIVE, head, len(payload), ext10) - entry = bytearray(DIR_ENTRY_SIZE) - entry[0:11] = self.encode_name(name) - entry[11] = ATTR_ARCHIVE - # Fixed timestamp: reproducible images matter more than real mtimes, - # since these get byte-compared across gate runs. - struct.pack_into(" finds it. If the subdir ALREADY exists (a second + # file staged into the same SAVES/), add INTO it rather than creating a duplicate root entry the + # guest's SUB lookup would never resolve. Human68k FAT12 is otherwise ordinary FAT12. dstem = dirname.upper() if len(dstem) > 8: raise ValueError(f"subdir '{dirname}' does not fit 8.3") - cb = self.cluster_bytes - need = max(1, (len(payload) + cb - 1) // cb) + main11, ext10 = self.encode_name_ext(name) + _, _, de = self.find(dirname) + if de is not None and (de[11] & ATTR_DIR): + head = self._alloc_write_payload(payload) + entry = self._dir_entry(main11, ATTR_ARCHIVE, head, len(payload), ext10) + self._put_entry_in_dir(struct.unpack("