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

215 lines
11 KiB
Markdown

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