llvm-mos / llvm-mos-sdk

SDK for developing with the llvm-mos compiler
https://www.llvm-mos.org
Other
267 stars 54 forks source link

Add the Apple II family as a target #102

Open TheHans255 opened 1 year ago

TheHans255 commented 1 year ago

I recently came across this project, and I was interested in using it to write code for my Apple //e. There is a demo of LLVM-MOS code running on the Apple //e on the website, but there aren't any target triples for it. Do you think we could add that to the list of platforms?

Ideally, I'd be interested in the following variations:

I'll note that I did attempt to write my own target definition inside my own project (using the instructions from the Embednomicon and basing it off the existing mos-unknown-none target), but my linker script did not appear to have access to any of the includes, including c.ld, imag-regs.ld, and crt0.S/crt0/crt. Here is what I have so far:

mos-apple-iie-none.json:

{
    "arch": "mos",
    "atomic-cas": false,
    "cpu": "mos6502",
    "data-layout": "e-m:e-p:16:8-i16:8-i32:8-i64:8-f32:8-f64:8-a:8-Fi8-n8",
    "disable-redzone": true,
    "linker": "mos-clang",
    "llvm-args": [
      "--force-precise-rotation-cost",
      "--jump-inst-cost=6",
      "--force-loop-cold-block",
      "--phi-node-folding-threshold=0",
      "--two-entry-phi-node-folding-threshold=0",
      "--align-large-globals=false",
      "--disable-spill-hoist"
    ],
    "llvm-target": "mos-unknown-none",
    "max-atomic-width": 8,
    "min-atomic-width": 8,
    "no-default-libraries": false,
    "panic-strategy": "abort",
    "requires-lto": true,
    "singlethread": true,
    "supports-stack-protector": false,
    "target-c-int-width": "16",
    "target-pointer-width": "16",
    "trap-unreachable": false,
    "vendor": "apple"
}

appleiielinker.ld (which I reference using the -Clink-arg=-Tappleiielinker.ld argument in config.toml):

/*
 * Apple //e linker script
 */

/* Available RAM goes from 0x0800 to 0x9600, skipping 0x2000-0x5fff
 * for HIRES graphics. The program will be loaded at 0x6000,
 * and the 6K below HIRES will be reserved for the soft stack
 */
MEMORY {
    ram (rw) : ORIGIN = 0x6000, LENGTH = 0x35ff
}

__rc0 = 0x0002;
/* INCLUDE imag-regs.ld */
__rc1 = __rc0 + 1;
PROVIDE(__rc2 = __rc1 + 1);
__rc3 = __rc2 + 1;
PROVIDE(__rc4 = __rc3 + 1);
__rc5 = __rc4 + 1;
PROVIDE(__rc6 = __rc5 + 1);
__rc7 = __rc6 + 1;
PROVIDE(__rc8 = __rc7 + 1);
__rc9 = __rc8 + 1;
PROVIDE(__rc10 = __rc9 + 1);
__rc11 = __rc10 + 1;
PROVIDE(__rc12 = __rc11 + 1);
__rc13 = __rc12 + 1;
PROVIDE(__rc14 = __rc13 + 1);
__rc15 = __rc14 + 1;
PROVIDE(__rc16 = __rc15 + 1);
__rc17 = __rc16 + 1;
PROVIDE(__rc18 = __rc17 + 1);
__rc19 = __rc18 + 1;
PROVIDE(__rc20 = __rc19 + 1);
__rc21 = __rc20 + 1;
PROVIDE(__rc22 = __rc21 + 1);
__rc23 = __rc22 + 1;
PROVIDE(__rc24 = __rc23 + 1);
__rc25 = __rc24 + 1;
PROVIDE(__rc26 = __rc25 + 1);
__rc27 = __rc26 + 1;
PROVIDE(__rc28 = __rc27 + 1);
__rc29 = __rc28 + 1;
PROVIDE(__rc30 = __rc29 + 1);
__rc31 = __rc30 + 1;

ASSERT(__rc31 == 0x0021, "Inconsistent zero page map.")

MEMORY { zp : ORIGIN = __rc31 + 1, LENGTH = 0x90 - (__rc31 + 1) }

SECTIONS {
    /* INCLUDE c.ld */
    .zp.data : { 
        __zp_data_start = .;
        *(.zp.data .zp.data.* .zp.rodata .zp.rodata.*) 
    } >zp AT>ram
    __zp_data_load_start = LOADADDR(.zp.data);
    __zp_data_size = SIZEOF(.zp.data);
    .zp.bss (NOLOAD) : { 
        __zp_bss_start = .;
        *(.zp.bss .zp.bss.*) 
    } >zp
    __zp_bss_size = SIZEOF(.zp.bss);
    .zp (NOLOAD) : { *(.zp .zp.*) } >zp
    .text : {
        /* A mechanism for dynamically building an _init script. */
        _init = .;
        _start = .;
        *(SORT_BY_INIT_PRIORITY(.init.* .init))
        *(.call_main)
        *(.after_main)

        /* A mechanism for dynamically building a _fini script. */
        _fini = .;
        *(SORT_BY_INIT_PRIORITY(.fini.* .fini))
        *(.fini_rts)

        *(.text .text.*)

        /* A sorted list of initialization function pointers. Used for GCC
        * constructor attribute and C++ global constructors. */
        __init_array_start = .;
        KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.* .init_array)))
        __init_array_end = .;

        /* A sorted list of finalization function pointers. Used for GCC destructor
        * attribute. */
        __fini_array_start = .;
        KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.* .fini_array)))
        __fini_array_end = .;
    }
    .rodata : { *(.rodata .rodata.*) }
    .data : { __data_start = .;
        *(.data .data.*)
        __data_end = .; 
    }
    __data_load_start = LOADADDR(.data);
    __data_size = SIZEOF(.data);
    .bss : { __bss_start = .;
        *(.bss .bss.* COMMON)
        __bss_end = .;
    }
    __bss_size = SIZEOF(.bss);
    .noinit (NOLOAD) : { 
        *(.noinit .noinit.*)
        __heap_start = .;
    }
}

/* 
 * Set the operand stack to the 6K memory region below HIRES
 */
__stack = 0x1FFF;

OUTPUT_FORMAT {
    TRIM(ram)
}

And the errors I get when compiling rust-hello-world-mos with this target triple:

error: linking with `mos-clang` failed: exit status: 1
  |
  = note: LC_ALL="C" PATH="/usr/local/rust-mos/lib/rustlib/x86_64-unknown-linux-gnu/bin:/vscode/vscode-server/bin/linux-x64/e2816fe719a4026ffa1ee0189dc89bdfdbafb164/bin/remote-cli:/home/mos/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" VSLANG="1033" "mos-clang" "/workspaces/rust-mos-hello-world/target/mos-apple-iie-none/debug/deps/rust_mos_hello_world-9618fd27fc545d4f.ufmt_stdio-e29ce7fa101f2a20.ufmt_stdio.76c6debd-cgu.0.rcgu.o.rcgu.o" "-Wl,--as-needed" "-L" "/workspaces/rust-mos-hello-world/target/mos-apple-iie-none/debug/deps" "-L" "/workspaces/rust-mos-hello-world/target/debug/deps" "-L" "/usr/local/rust-mos/lib/rustlib/mos-apple-iie-none/lib" "-Wl,-Bstatic" "/workspaces/rust-mos-hello-world/target/mos-apple-iie-none/debug/deps/libcompiler_builtins-cae85a8e15dfddde.rlib" "-Wl,-Bdynamic" "-Wl,--eh-frame-hdr" "-Wl,-znoexecstack" "-L" "/usr/local/rust-mos/lib/rustlib/mos-apple-iie-none/lib" "-o" "/workspaces/rust-mos-hello-world/target/mos-apple-iie-none/debug/deps/rust_mos_hello_world-9618fd27fc545d4f" "-Wl,--gc-sections" "-no-pie" "-Wl,-O1" "-Tappleiielinker.ld"
  = note: clang-16: warning: argument unused during compilation: '-no-pie' [-Wunused-command-line-argument]
          ld.lld: warning: /workspaces/rust-mos-hello-world/target/mos-apple-iie-none/debug/deps/libcompiler_builtins-cae85a8e15dfddde.rlib: archive member 'lib.rmeta' is neither ET_REL nor LLVM bitcode
          ld.lld: error: unable to find library -l:crt0.o
          ld.lld: error: unable to find library -lcrt0
          ld.lld: error: unable to find library -lcrt
          ld.lld: error: unable to find library -lc
          clang-16: error: ld.lld command failed with exit code 1 (use -v to see invocation)
johnwbyrd commented 1 year ago

https://llvm-mos.org/wiki/Porting . At one point I did get a BASIC stub for the Apple IIx running with llvm-mos; you may need to follow the Commodore 64 examples to resurrect it.

TheHans255 commented 1 year ago

https://llvm-mos.org/wiki/Porting . At one point I did get a BASIC stub for the Apple IIx running with llvm-mos; you may need to follow the Commodore 64 examples to resurrect it.

Thanks for the tip. It turned out that I should have been using the "mos-common-clang" linker instead of "mos-clang". The linker script looks a lot more sane now (since I can actually include c.ld and the others), and I've managed to create some toy examples for the Apple II in C, including some using printf(). The parallel attempts in Rust have been less successful (instead producing a file containing some gibberish and the string "gdb_load_rust_pretty_printers.py", but I'll keep working on those.

Is there any particular advantage to using a BASIC stub instead of a DOS binary file on the Apple II? I find the latter easier to generate, though I reckon that BASIC programs might be easier to port (right now I have to use Ciderpress to get the binary image onto an Apple emulator).

johnwbyrd commented 1 year ago

Most Apple IIx users will be familiar with the LOAD command, less so with the BLOAD command. So I went with the BASIC stub solution. Makes it a bit more consistent with the other platforms.

TheHans255 commented 1 year ago

Most Apple IIx users will be familiar with the LOAD command, less so with the BLOAD command. So I went with the BASIC stub solution. Makes it a bit more consistent with the other platforms.

That makes sense, and we could probably do something similar with a CALL instruction in BASIC. Of course, if we want to make the HIRES pages available, the stub will also need to include a copy routine to get the program to $6000, which is also what we would need to write to properly relocate a ProDOS system program for the same reason.

Of course, another option we have on Apple is creating a fully bootable disk, which is probably even more familiar to Apple users than the LOAD command is.

TheHans255 commented 1 year ago

I've started building a repository to perform the work towards building an Apple //e port for LLVM-MOS: https://github.com/TheHans255/apple-ii-port-work

My current focus is on writing text/CLI programs on top of ProDOS, with the goal of eventually recreating a good portion of the C stdlib in order to port programs such as Vi and Telnet. So far, I have written getchar()/putchar() using COUT/RDCHAR, and have created a ProDOS system call interface with endpoints for OPEN, READ, WRITE, and CLOSE. In the immediate future, I plan to expose more of these syscalls and more System Monitor calls, and also provide alternate linking strategies for writing plain Apple, DOS 3.3, or HIRES-capable programs, before submitting a port to this repository.

sehugg commented 9 months ago

This is good! I am interested in binaries that can be wholly loaded into memory and that do not use ROM routines or DOS. I think that could just be a matter of library and crt0 configuration. It would also be nice to override getchar() putchar() and other functions, e.g. for a hi-res character routine.

I am not sure what executable format would be best for this, something like Atari's XEX seems useful for handling multiple segments but I'm not aware of anything similar for Apple ][. Right now I just prepend a two-byte header with the start address a la C64 PRG and fill as much RAM as possible. A good binary format would let you skip over DOS, fill language card areas, etc.

My use case is quick-loading into an emulator (faking a disk drive, stuffing RAM and jumping to $803) but it would be nice to turn the executable into a bootable disk. Or at least have an executable format that makes it easy to do so. And of course BASIC stubs and plain old binary files are useful too. There's also AppleSingle but I am not very familiar.

TheHans255 commented 9 months ago

I am interested in binaries that can be wholly loaded into memory and that do not use ROM routines or DOS. I think that could just be a matter of library and crt0 configuration.

I think my efforts should support this. I'm currently developing my SDK additions in multiple tiers that assume different amounts about the runtime environment. Currently, one is just Apple II plus ROM routines, one adds ProDOS, another overrides getchar() putchar() and exit() on top of ProDOS, and a few more set linker settings for different start locations. I can easily add more for different use cases, including DOS 3.3 programs and arbitrarily loaded subroutines (i.e. for BASIC programs or linked libraries).

It would also be nice to override getchar() putchar() and other functions, e.g. for a hi-res character routine.

Totally. My tiered system should help with this too - in fact, I may want to reconfigure things so that these are dependencies that get pulled in.

I am not sure what executable format would be best for this, something like Atari's XEX seems useful for handling multiple segments but I'm not aware of anything similar for Apple ][. Right now I just prepend a two-byte header with the start address a la C64 PRG and fill as much RAM as possible.

I'm not aware of anything like that either. I imagine the next best thing is multiple files and a bootloader program that loads them from DOS in the right places and swaps them in and out as needed, which is definitely very useful if you have a RAM disk, but probably defeats the simplicity that you're looking for.

A good binary format would let you skip over DOS, fill language card areas, etc.

As it happens, as long as it fits your use case (IIe and above), ProDOS lives in the Language Card area, so that could help with the segmentation problem.

My use case is quick-loading into an emulator (faking a disk drive, stuffing RAM and jumping to $803) but it would be nice to turn the executable into a bootable disk. Or at least have an executable format that makes it easy to do so. And of course BASIC stubs and plain old binary files are useful too. There's also AppleSingle but I am not very familiar.

Agreed on those. My current route with ProDOS is to output a System program and use AppleCommander to write it to a bootable disk, and it would probably be nice to streamline that a little bit. I imagine that a bootable DOS 3.3 disk wouldn't be much trouble to generate from scratch.

sehugg commented 9 months ago

For the "DOS-free ROM-ignoring" target, I think the linker can be coaxed to output a full .DSK image with bootloader. It could load as much RAM as needed, and the program could optionally use a RWTS library for overlays or additional data. It'd be nice for testing, since the SDK wouldn't need external tools or disk images to produce an emulator-ready product. This might not have a lot of overlap with what you're doing, except for sharing of common "bare metal" (for lack of a better word) routines.

I agree that ProDOS is the priority. DOS 3.3 might be nice for completeness sake, but I don't know if it supports many use cases that ProDOS or raw bootloader doesn't.

TheHans255 commented 9 months ago

For the "DOS-free ROM-ignoring" target, I think the linker can be coaxed to output a full .DSK image with bootloader. It could load as much RAM as needed, and the program could optionally use a RWTS library for overlays or additional data. It'd be nice for testing, since the SDK wouldn't need external tools or disk images to produce an emulator-ready product. This might not have a lot of overlap with what you're doing, except for sharing of common "bare metal" (for lack of a better word) routines.

OK, I think I get it: you would like to have, as an execution target, a disk that is entirely dedicated to the app/game/test program and is not necessarily compatible with any existing OS, in which the 256-byte bootloader at the start of the disk just loads your program directly. Given the format of .DSK files, that should be more than reasonable, and I agree that would be super useful for both quick design iterations and games that want the entirety of a single disk.

I'm not sure exactly how much time I'd want to spend building a Read Write Track Sector library, but I'll be sure to provide whatever platform support we'd need for that.

I agree that ProDOS is the priority. DOS 3.3 might be nice for completeness sake, but I don't know if it supports many use cases that ProDOS or raw bootloader doesn't.

Agreed. All I can think of really is 1) earlier Apple models that don't support ProDOS (since ProDOS 2.0.0+ uses 65C02 instructions), and 2) interacting with DOS 3.3 disks specifically.

sehugg commented 9 months ago

OK, I think I get it: you would like to have, as an execution target, a disk that is entirely dedicated to the app/game/test program and is not necessarily compatible with any existing OS, in which the 256-byte bootloader at the start of the disk just loads your program directly.

Yes, exactly! I've even thrown together a CC65 prototype using 0boot that seems to work, barring some language card issues. The bootloader destroys zero page, which is fine if avoiding ROM routines and using all 256 bytes for your program, otherwise it'll have to be re-initialized.

I'm not sure exactly how much time I'd want to spend building a Read Write Track Sector library, but I'll be sure to provide whatever platform support we'd need for that.

Yeah, that might be a bonus feature. I thought it would be easier to find a standalone RWTS, but surprisingly not, except for that 18-sector version used in Prince of Persia.

TheHans255 commented 9 months ago

I've even thrown together a CC65 prototype using 0boot that seems to work, barring some language card issues. The bootloader destroys zero page, which is fine if avoiding ROM routines and using all 256 bytes for your program, otherwise it'll have to be re-initialized.

This is nice! Unfortunately, I'm not sure I quite have the chops to do it myself (and IMO, I'm not quite sure it's a great fit for the vanilla SDK), but I believe that LLVM-MOS's linker scripts should be more than powerful enough to recreate this flow.

Since it seems that we can also accomplish your flow with a handful of post-build steps (i.e. after compiling your C program as a blob, assemble an appropriately configured 0BOOT and concatenate the C program to it to form your disk file), something I can ensure we have is a compiler target that assumes full use of RAM but does not give access to any ROM or DOS routines, emitting a binary blob. This blob should be sufficient to load into an emulator or real device via 0BOOT, but it would also be flexible enough to load onto the Apple II via some other method (e.g. put it on a DOS 3.3 disk to be loaded with BRUN, load it through the audio port with c2t, translate it to Monitor store instructions to be sent over the serial port, stuff an Apple II emulator's save state with the data, etc.)

asiekierka commented 5 months ago

@TheHans255 Any news on this? Even if partial, it would be good to upstream some Apple // support :-)

TheHans255 commented 5 months ago

@TheHans255 Any news on this? Even if partial, it would be good to upstream some Apple // support :-)

Thanks for asking! I think we could get some stuff upstreamed in soon. Past needing to update my work to match the latest version of the SDK, I guess the main thing blocking upstream integration is the question of which configurations of Apple II machine code we're interested in supporting as a main part of the SDK.

The Apple II has a lot of valid choices for deployment/memory regions, in which pretty much any combination of the following is valid:

All of these options require alternate linking and deployment strategies, and some of them require additional libraries. Still others present complications for both malloc and printf, which are included implicitly in mos-common-clang with no real option to replace them.

Which combinations are we interested in including as a base part of the SDK? I would be OK settling on some core bits of basic functionality and maintaining a separate set of plugin targets for the other combinations, but I would want to know which parts of the functionality we want to support.

asiekierka commented 5 months ago

This is a question better suited for @johnwbyrd , but I can give my personal opinion, at least.

So far, we generally appear to strive towards supporting "common" configurations - that is, configurations for writing computer programs that people are likely to end up using; be it due to convention, parts availability, or other factors. For example:

We also established that LLVM-MOS-SDK is an SDK that tries to be relatively unopinionated while being effective enough for such "common" confgiurations. We acknowledge that other SDKs could pop up based around the LLVM-MOS compiler which target a given platform with higher specificity. However, the NES already has eight subtargets (and issues open for a few more), while the Atari 8-bit computer line has four. If the complexity is compartmentalized and the maintenance cost of such subtargets is low, there is no issue with adding more of them.

I would say that, at the very least, it is not our intention to support the use-case of "subroutine to an existing BASIC program" - LLVM-MOS-SDK produces independent software files as output. Beyond that, I don't really know the Apple II scene enough to understand which configurations are commonly used for creating software in the present day, while which are curiosities only.

Regarding some of the technical issues, which is an area I'm more involved in:

I leave a proposal of how to arrange the Apple II targets to you, but here's a very rough GUESS on what I would do based on about ten minutes of researching the hardware:

asiekierka commented 5 months ago

Sorry for the slightly messy comment, but it's tricky to provide concrete answers for hardware I've never worked with... I hope this at least gives some insight/ideas.

TheHans255 commented 3 months ago

Update: Unfortunately, I don't think I'll have the time to complete my work on this anytime soon and upstream Apple II compatibility to this SDK. Life has gotten busier for me lately, and I especially don't have the time I need to keep up with the multiple breaking changes that have been made to the standard library.

I'll probably continue work on my own repositories, likely focusing on alternate implementations of stdio and malloc based on ProDOS (since that most closely fits with my original intended use case). In the meantime, if someone else has the time and interest to upstream Apple II compatibility, they are welcome to do so.