joeylib2/LLVM816-ASKS.md
2026-08-20 01:22:50 -05:00

11 KiB

Asks for the llvm816 toolchain session

JoeyLib-side findings that need toolchain-side fixes. Context: PERF-AUDIT.md (findings #79, #77) and PERF-AUDIT-PLAN.md Phase 1. JoeyLib does not modify toolchains/iigs/llvm-mos per the ownership split -- these are requests, with evidence.

1. MVN-based memset/memcpy in runtime/src/libc.c (PERF-AUDIT #79)

memset, memcpy (and memmove's copy loops) in toolchains/iigs/llvm-mos/runtime/src/libc.c are one-byte-at-a-time loops through 24-bit pointers:

void *memset(void *dst, int c, size_t n) {
    char *d = (char *)dst;
    while (n--) *d++ = (char)c;
    return dst;
}

That is ~30 cycles/byte on the 65816. The ORCA/C baseline these replaced lowers both to MVN block moves (~7 cycles/byte after setup).

Measured impact on the reference platform (UBER, honest post-Phase-0 clock): jlScbSetRange -- a 200-byte memset plus a flag write -- measures 163 ops/sec on the clang build vs 1005 ops/sec on the ORCA-era capture (~4x regression at the whole-op level). Every memset/memcpy-based JoeyLib path pays it: surface copies, sprite save/restore fallbacks, SCB range fills, stage-alloc clears, the log buffer, and all example/game code.

Ask: MVN-based memset/memcpy in libc.c (or intrinsic lowering in the backend). Requirements from the JoeyLib side:

  • Handle bank-crossing source/destination (MVN wraps within a bank; the routine must split runs at bank boundaries).
  • memset needs a 1-byte seed write + overlapping MVN (classic self-copy fill), or an unrolled STA loop for short fills.
  • Keep the size_t=unsigned long (32-bit) signature -- JoeyLib call sites pass 32-bit counts today and the current behavior is correct, just slow.
  • A short-count fast path (n < ~16) matters too: the sprite interpreter's per-row copies are 4-17 bytes.

2. FYI: sprite-codegen corruption under investigation (PERF-AUDIT #77)

JoeyLib's IIgs runtime sprite code generation corrupts memory when enabled (bisected; disabled by default in spriteCompile.c until root-caused). The current suspect list is JoeyLib-side (emit-time buffer handling), but if the hunt lands on codegen/ABI behavior of clang-emitted calls into arena-resident routines, it becomes a toolchain question. Nothing to do yet -- flagged so the context is not a surprise if it arrives.

3. fflush -> FST commit semantics (PERF-AUDIT #80)

JoeyLib's logger (src/core/debug.c) fflush()es after every line so a crash/hang leaves the log on disk. On the clang runtime the lines do not reach the disk image until fclose: a run that executed 33 logged operations left 3 lines on disk after a forced emulator exit. Question: does the libc fflush propagate to a GS/OS FST flush for the file, or only drain the userland buffer? If the latter, an FST-flush call in fflush (or an exposed fsync-like hook) would restore crash-durable logging. Not urgent -- JoeyLib works around it with a memory mailbox under emulation -- but real-hardware crash forensics wants it.

4. malloc/calloc returns 1 (not NULL) at heap exhaustion (PERF-AUDIT #81) -- CRITICAL

Empirically confirmed under MAME with in-app probes: with the default link-layout heap (665 bytes at $00:BC67-$00:BF00 in the JoeyLib UBER build), calloc(1, ~720) returns 1. The bump-path overflow branch in libc.c malloc -- whose comment already records a "historical p + HDR_SZ + n > heapEnd over-heap miscompile (only manifested for oversized n)" and assumes the path is unexercised -- is still miscompiled in exactly that oversized case. Downstream, JoeyLib ran for weeks on a phantom struct at (void*)1 whose first field happened to alias the real framebuffer pointer; see PERF-AUDIT.md #81 for the full forensics. JoeyLib no longer allocates anything large on the C heap, but ANY program that exhausts this heap gets silent corruption instead of NULL. Minimal repro: link any GS/OS app with default heap, call calloc(1, 720), observe the return value.

5. Segment BSS/heap spans are not reserved from the Memory Manager (PERF-AUDIT #81/#77/#85) -- CRITICAL, EMPIRICALLY PROVEN

Three independent confirmations, in escalating severity:

  1. The bank-0 C heap (unreserved) -- #81's phantom-stage chain.
  2. The pinned SHR framebuffer $01:2000-$9CFF (unreserved) -- an MM handle landed at $01:8B04 inside the display (#85).
  3. THE GENERAL CASE: a 32KB NewHandle was placed at $08:4200 -- inside the app's own ENTRY-SEGMENT BANK, overlapping seg1's BSS span. The loader only claims each segment's image bytes; everything above (BSS, heap) is MM-free. ANY GS/OS allocation -- the app's own or the system's -- can land on live program data.

JoeyLib has stopgap claims for cases 1-2 (jlpInit reserves the framebuffer and $00:B900-$BEFF). Case 3 was attempted app-side on 2026-07-05 (spacetaxi acceptance test) and the attempt produced two findings you should have:

(a) The app CAN learn its BSS spans: the linker-emitted __bss_seg{0..3}_{lo16,bank,size} symbols (the ones crt0.s zeroes BSS through) are readable from C via a .long DATA32 table in an asm TU -- direct C IMM16 references fail to link ("cross-segment reloc ... uses 2-byte form; only IMM24 / DATA32 supports cINTERSEG patching"). The DATA32 entries resolve as RELOCATED FULL ADDRESSES: the lo16 entry reads back as the 24-bit runtime span base ($08:4F00 observed for the spacetaxi build), the size entry carries the length in its low 16 bits, empty segments read as bank-base-only. Recipe verified working end-to-end under MAME.

(b) BUT actually claiming the span kills the app: NewHandle( attrAddr|attrFixed|attrLocked, base=$08:4F00, size=$6619) from jlpInit SUCCEEDS -- and the app then dies before reaching main's first log line, with the PC profile showing GS/OS desktop idle (silent return-to-Finder). Reproduced twice; reverting only the claim restores the previous behavior. Mechanism unknown from the app side -- presumably the Loader/MM bookkeeping objects to a user handle overlapping a loaded segment's address range, or the claim starves something during the remaining init. The reverted implementation was never committed; the full code (asm table + claim loop) is reproduced verbatim in the copy of this item handed to the toolchain session at /home/scott/claude/llvm816/JOEYLIB-ASKS.md.

Ask: crt0/omfEmit/loader cooperation so every segment's FULL in-memory span (image + BSS + heap) is Memory-Manager-reserved at load time -- with (b) suggesting the reservation must be made BY the loader glue (or before/while segments are registered), not by app code after the fact. This single fix unblocks IIgs sprite codegen (PERF-AUDIT #77), the Space Taxi IIgs port (a 32KB font-surface jlpBigAlloc lands on live state layout-dependently -- three distinct failure modes observed as the binary layout shifted), and removes the whole corruption class.

link816 defines [__heap_start, __heap_end) as the leftover entry-bank space above BSS, but crt0Gsos never claims that range from the GS/OS Memory Manager, so NewHandle can legally place blocks on top of live malloc data (a second, independent corruption channel -- and the crt0Gsos comments already record a Phase-1.1 fopen-hang caused by this class). Ask: reserve the heap range at startup (attrAddr|attrBank| attrFixed|attrLocked), or reimplement malloc on top of an MM handle. Related hardening: pass attrNoSpec on NewHandle calls in the runtime, and consider reserving the pinned SHR stage range $01:2000-$9FFF that JoeyLib's jlpStageAllocPixels hands out as a bare constant.

6. Post-main exit path crashes to the monitor (COP $00 at $00:0002) -- EMPIRICALLY PROVEN, app-independent (2026-07-20)

Every JoeyLib IIgs example that RETURNS from main() dies in the monitor instead of quitting to Finder. Reproduced identically for KEYS and STAXI under MAME (deterministic, ~3 min): press the app's exit key, and execution ends at COP #$00 executing at $00:0002 with S=$17FD. Nobody saw this before because every harness kills the emulator after reading logs; an interactive ESC was the first real exit ever exercised.

MAME debugger execution history at the crash (bp at $000002):

00:9EE0: sta $00c068     ; GS/OS call tail: state reg restore
00:9EE8: xce
00:9EE9: plp
00:9EEA: rtl             ; returns to the app-side thunk:
08:0072: php             ; <- pushes 1 byte...
08:0073: rtl             ; <- ...then pulls 3: garbage return
00:0002: cop #$00        ; monitor

The thunk at segment offset $0072 (bank $08 at run time) executes php / rtl after a GS/OS call returns to it -- the php mis-balances the stack and the rtl returns to $00:0002. This is in the crt0Gsos / runtime exit code (the offset is constant across different apps, so it is runtime code, not app code). Suspects: a Quit stub whose body was meant to sit between the php and the rtl, or a mis-assembled epilogue.

Reproducer scripts (MAME, headless, breakpoint + history + RAM dump): this session's scratchpad escCrash.sh / escCrashKeys.sh -- ask me and I'll re-stage them anywhere durable.

WORKAROUND LANDED IN JOEYLIB (so this is no longer user-visible): src/iigs/hal.c jlpShutdown now issues GS/OS QuitGS ($2029, pCount=0) itself via a runtime-built jsl stub and never returns to the runtime exit path. jlShutdown therefore does not return on IIgs. The root cause still deserves a runtime fix: any app that returns from main WITHOUT calling jlShutdown (or any non-JoeyLib program built on this runtime) still crashes.

7. Volatile uint8_t loads through variable pointers widen to 16-bit bus reads (2026-08-18) -- EMPIRICALLY PROVEN

A volatile uint8_t LOAD through a variable (long) pointer is emitted as a 16-bit lda [dp],y with m=0 and the value masked afterwards (and #$ff) -- so the BUS performs a phantom read of address+1. For memory that is harmless; for I/O it reads a neighbouring register, and hardware with adjacent registers has read side effects. Concrete damage on the IIgs SCC (Z8530, four adjacent registers $C038-$C03B): a data-register read of channel B ($C03A) phantom-pops channel A's RX FIFO ($C03B -- the AppleTalk port), and in a printer-port configuration a status poll of $C039 phantom-pops the MODEM port's data at $C03A. (A per-poll RR1 harvest also correlates with MAME 0.264 SCC RX-deafness, but that is NOT this bug: it reproduced through a guaranteed 8-bit read too, so the phantom read is refuted as its mechanism and the harvest ships disabled under emulation. The width violation itself stands proven by disassembly.)

The width handling is inconsistent, which is what makes it look like a bug rather than a policy:

  • STORES through the same pointers are correctly sep #$20-wrapped 8-bit.
  • LOADS from CONSTANT addresses are correctly sep-wrapped 8-bit (sep #$20 / lda $c038 / rep #$20 -- verified via a probe TU).
  • Only variable-pointer loads take the 16-bit lda [dp],y + mask form.

Ask: honour the declared access width for volatile loads in the long-indirect form -- sep-wrap them exactly like the constant-address and store cases.

WORKAROUND LANDED IN JOEYLIB: src/iigs/serial.c routes every serial register read through a hand-asm jlpIoRead8 (global function, so no call site can be inlined back into the widened form): sep #$20 / lda [$e0],y / rep #$30 / 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.