; crt0 — C runtime startup for the W65816 backend. ; ; Entry point invoked by the loader (or the OMF dispatcher). Sets up ; the processor mode the rest of the runtime expects, zeroes BSS, ; calls main, and halts on return. ; ; Conventions: ; - Native mode (E=0), 16-bit M and X (REP #$30) on entry to main. ; - DP=0, DBR=0 — assumed by the C runtime. ; - Linker-emitted symbols: __bss_start, __bss_end (16-bit addrs). .text .globl __start __start: ; Disable IRQ first — the IIgs ROM hands a vsync IRQ on every frame, ; and its handler runs in 8-bit M/X mode, corrupting our state if ; we leave I clear. SEI is fine in either emulation or native ; mode and is always 1 byte / 2 cycles. sei ; Native mode + 16-bit registers. clc xce rep #0x30 ; Disable IIgs peripheral interrupt sources at the chip level — ; SEI alone leaves the hardware lines asserted, and the IRQ trap ; in ROM keeps re-firing if the source isn't quiesced. STZ ; stores zero without going through A; in M=8 it stores 1 byte ; (matching the 8-bit registers), so no LDA #0 prelude is needed. sep #0x20 stz 0xc041 ; INTEN = 0 (clear AN3/mouse/0.25s/VBL/mouse-IRQ enables) stz 0xc023 ; VGCINT = 0 (clear external/1-sec/scan-line IRQ enables) stz 0xc032 ; SCANINT clear rep #0x20 ; Top-of-stack at $0FFF. Native-mode S is 16-bit, so we don't need ; to stay in page 1. Soft-double frames can be ~170 bytes plus the ; usual call-chain overhead — at $01FF stack growth wraps into the ; direct page ($0000-$00FF) which holds our libcall scratch ; ($E0-$F4) and IMG slots ($D0-$DE), corrupting them. $0FFF gives ; ~3.5 KB of headroom and stays below the text base ($1000). lda #0x0fff tcs ; Enable Language Card RAM at $D000-$FFFF for read+write. This ; is 12KB (4KB at $D000-$DFFF in LC bank 1, plus 8KB at ; $E000-$FFFF common LC area). The IIgs LC area defaults to ; ROM-mapped; two reads of $C083 enable bank-1 RAM read AND ; write for the whole $D000-$FFFF range. link816 may auto- ; relocate BSS / heap into this area when text+rodata grows ; past $BFFF — without this enable, writes drop on the floor ; and reads return ROM bytes. The reads must be 8-bit (one ; byte at a time) — a 16-bit M read at $C083 would also touch ; $C084 (a different soft switch), wiping the LC enable. sep #0x20 lda 0xc083 lda 0xc083 rep #0x20 ; Persistent "current data bank" byte at DP $BE. The LDAptr/ ; STAptr/STBptr inserters load this into $E2 before each [dp],Y ; deref so pointer-deref lands in the user's bank, matching where ; DBR-relative absolute stores go. Under MAME (no Loader), DBR=0 ; and PBR=0 here, so $BE=0 — equivalent to the prior STZ $E2 ; behavior. Under GS/OS Loader, crt0Gsos.s sets it to PBR. ; ; $BE chosen because it's outside both the libcall scratch range ; ($E0..$FF used by libgcc.s for i64 ops) and the IMG slot range ; ($C0..$DE). PHK pushes 1 byte; PLA in M=8 to pull just 1 byte. sep #0x20 phk pla ; A's low byte = current PBR sta 0xbe ; persistent data bank stz 0xbf ; pad: `lda 0xbe` in 16-bit M loads $00PBR ; — used by codegen for &symbol bank halves rep #0x20 ; Zero BSS. Up to 4 segments — linker emits __bss_seg{0..3}_lo16 ; / _bank / _size symbols. Segments with size=0 are skipped. ; Each segment is cleared with DBR-relative STZ abs,X after ; setting DBR to the segment's bank. Original DBR restored at ; end via PLB. rep #0x10 ; ensure X is 16-bit phb ; save current DBR ; ---- segment 0 ---- ; The `sep #0x20` that sets the segment's data bank also puts us in ; M=8 for the whole store loop (X stays 16-bit), so we don't flip ; SEP/REP per byte; each segment label below re-enters via `rep`. rep #0x20 ldx #__bss_seg0_size beq .Lbss_seg1 sep #0x20 .a8 ; following lda #imm8 uses 1-byte immediate lda #__bss_seg0_bank pha plb ldx #0 .Lbss_loop0: cpx #__bss_seg0_size bcs .Lbss_seg1 stz __bss_seg0_lo16, x inx bra .Lbss_loop0 .Lbss_seg1: ; ---- segment 1 ---- rep #0x20 ldx #__bss_seg1_size beq .Lbss_seg2 sep #0x20 .a8 ; following lda #imm8 uses 1-byte immediate lda #__bss_seg1_bank pha plb ldx #0 .Lbss_loop1: cpx #__bss_seg1_size bcs .Lbss_seg2 stz __bss_seg1_lo16, x inx bra .Lbss_loop1 .Lbss_seg2: ; ---- segment 2 ---- rep #0x20 ldx #__bss_seg2_size beq .Lbss_seg3 sep #0x20 .a8 ; following lda #imm8 uses 1-byte immediate lda #__bss_seg2_bank pha plb ldx #0 .Lbss_loop2: cpx #__bss_seg2_size bcs .Lbss_seg3 stz __bss_seg2_lo16, x inx bra .Lbss_loop2 .Lbss_seg3: ; ---- segment 3 ---- rep #0x20 ldx #__bss_seg3_size beq .Lbss_done sep #0x20 .a8 ; following lda #imm8 uses 1-byte immediate lda #__bss_seg3_bank pha plb ldx #0 .Lbss_loop3: cpx #__bss_seg3_size bcs .Lbss_done stz __bss_seg3_lo16, x inx bra .Lbss_loop3 .Lbss_done: rep #0x20 ; back to M=16 after the M=8 store loop plb ; restore caller's DBR ; Run static constructors. The linker emits ; __init_array_start / __init_array_end around the .init_array ; section; under p:32:16 each entry is a 32-bit function pointer ; (low 16 bits = function offset, high 16 bits = bank, 0 for our ; single-bank programs). Walk in 4-byte stride and JSL each via ; __jsl_indir using only the low half. rep #0x30 ; native, 16-bit M and X ldx #__init_array_start .Linit_loop: cpx #__init_array_end bcs .Linit_done ; __jsl_indir does `JMP (__indirTarget)` — reads a 16-bit ptr ; from __indirTarget and JMPs there. So __indirTarget must ; hold the function pointer itself (NOT the address of the ; init_array slot). Dereference the entry: ($E0)→A. stx 0xe0 ; entry addr -> DP scratch ldy #0 lda (0xe0), y ; A = mem[X] (DP-indirect-Y, opcode 0xb1) sta 0xb8 ; __indirTarget pinned at DP $B8 (bank-0 fixed; see libgcc.s) phx ; preserve X across the call jsl __jsl_indir plx ; Step by 4 bytes (sizeof(void*) under p:32:16). inx inx inx inx bra .Linit_loop .Linit_done: ; Tool Locator init is the OS's job, not crt0's: ; - GS/OS Loader (crt0Gsos.s path) pre-inits TL before transferring ; control to __start. helloBeep proves this: it does `SysBeep` ; (JSL $E10000 dispatch) with zero init in user code and works. ; - GNO kernel (crt0Gno.s path) uses JSL $E10000 itself, so TL is ; up before __start runs. ; - Bare metal (this crt0.s): ROM pre-boot has already run ; TLBootInit/MMBootInit; libc.c's iigsToolboxInit() does a per- ; process TLStartUp + explicit SEI for clients of time()/clock(). ; Do NOT inline TLStartUp here — it CLI's internally, and the ROM ; VBL handler runs in M=X=8 and would corrupt our in-progress ; M=16 register state (the most plausible root of the earlier ; attempt's "MAME segfault" mystery). ; Programs that use per-process tools (MM/QD/EM/WM) must call ; MMStartUp() and per-tool StartUps themselves — typically via ; iigs/desktop.h's startdesk(640) helper. ; Set DBR to the BSS bank before calling main. Without this, the ; user has to manually switch the data bank register (with a ; `pha;plb` inline-asm idiom) before doing absolute data ; accesses. Calypsi's "small data model" assumes DBR is ; pre-set; we now follow the same convention. ; ; Linker emits __bss_seg0_bank as the bank byte of the first ; BSS segment (typically 2 with the default --bss-base 0x020000). ; Programs that need bank-0 access (e.g., for $C0xx soft ; switches) can use STA_Long or temporarily switch DBR back. sep #0x20 .a8 ; following lda #imm8 uses 1-byte immediate lda #__bss_seg0_bank pha plb rep #0x20 .a16 ; restore M=16 for the code that follows ; Call main. Standard W65816 ABI: i16 first arg in A; we pass ; nothing. After return, A holds the exit code. jsl main ; Run global dtors registered via __cxa_atexit (LIFO order). No ; OS exit on bare metal — they run before we spin. A holds main()'s ; return; preserve it (harmless on bare metal but keeps the ; convention uniform across crt0 variants). pha jsl __run_cxa_atexit pla ; main returned. Bare metal has no OS to return to, so spin. ; (Previously a BRK $00 here, but headless MAME mis-vectors BRK to ; $0000 and wild-jumps, which reads as a crash. A tight loop is the ; conventional bare-metal halt; IRQs are already masked above, so ; this just idles.) .Lhalt: bra .Lhalt .size __start, . - __start