joeylib2/LLVM816-ASKS.md

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