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