935 lines
42 KiB
Markdown
935 lines
42 KiB
Markdown
# Oscar64 — Learnings
|
||
|
||
Notes from exploring the [Oscar64](https://github.com/drmortalwombat/oscar64) C/C++
|
||
cross-compiler that targets the 6502 family (C64, C128, PET, VIC-20, Plus/4, NES,
|
||
Atari 8-bit, Commander X16, Mega 65). Source tree was copied from `~/temp/c64/oscar64`
|
||
into `./oscar64`.
|
||
|
||
## 1. What it is, in one paragraph
|
||
|
||
Oscar64 is a native (not LLVM, not CC65) C/C++ compiler whose whole reason to
|
||
exist is "fast, dense 6502 code from C". The author expected 6502 to be a bad C
|
||
target — small register file, no stack-relative addressing, 256-byte hardware
|
||
stack, no native 16-bit ops — and tried a bytecode interpreter first. The
|
||
interpreter won on size but lost badly on speed, so they switched to native code
|
||
generation and discovered that careful analysis actually beats the interpreter on
|
||
both axes. The C64 hits 442 dhrystone V2.2 iterations/sec at `-O3`, which is
|
||
the kind of number that makes you look twice. The compiler is self-contained
|
||
(no external assembler / linker / CRT), supports disk overlays and banked
|
||
cartridges for big projects, and has been used to ship multiple real games.
|
||
|
||
## 2. Repository layout
|
||
|
||
```
|
||
oscar64/
|
||
├── oscar64/ # The compiler itself (C++17, ~50 .cpp/.h files)
|
||
│ ├── oscar64.cpp # CLI driver / main()
|
||
│ ├── Compiler.{h,cpp} # Orchestrator: ParseSource() → GenerateCode() → WriteOutputFile()
|
||
│ ├── Preprocessor.{h,cpp} # Macros, #embed, #for, #repeat
|
||
│ ├── Scanner.{h,cpp} # Tokenizer
|
||
│ ├── Parser.{h,cpp} # C99 + big chunks of C++
|
||
│ ├── Declaration.{h,cpp} # AST / symbol tables
|
||
│ ├── InterCode.{h,cpp} # Target-independent IR (the "IC_*" ops)
|
||
│ ├── InterCodeGenerator.{h,cpp} # AST → IR
|
||
│ ├── GlobalAnalyzer.{h,cpp} # Whole-program analysis
|
||
│ ├── GlobalOptimizer.{h,cpp} # Whole-program opts (inlining, const-prop, dead code)
|
||
│ ├── Constexpr.{h,cpp} # Compile-time evaluation
|
||
│ ├── ByteCodeGenerator.{h,cpp} # Optional bytecode backend (-bc)
|
||
│ ├── NativeCodeGenerator.{h,cpp} # 6502 native backend (the hot path)
|
||
│ ├── NativeCodeOutliner.{h,cpp} # -Oo: extract repeated code sequences
|
||
│ ├── Assembler.{h,cpp} # 6502 assembler + macros
|
||
│ ├── Linker.{h,cpp} # Region/section/object layout
|
||
│ ├── DiskImage.{h,cpp} # .d64 writer
|
||
│ ├── Emulator.{h,cpp} # Built-in 6502 emulator for -e/-ep
|
||
│ ├── Compression.{h,cpp} # LZO + RLE for #embed
|
||
│ ├── Disassembler.{h,cpp} # For .lbl / .asm output
|
||
│ └── ...
|
||
├── include/ # Runtime headers + stdlib (what compiled programs #include)
|
||
│ ├── crt.c, crt.h # Target-specific startup (linked unless -rt=)
|
||
│ ├── oscar.c, oscar.h # oscar_expand_lzo / oscar_expand_rle
|
||
│ ├── c64/ # VIC, CIA, SID, kernalio, rasterirq, memmap, sprites…
|
||
│ ├── c128/, vic20/, plus4/, nes/, cx16/, opp/, audio/
|
||
│ ├── gfx/ # bitmap, bitmapmc, vector3d, blob…
|
||
│ ├── new/ # C++ new/delete
|
||
│ └── stdio.h, stdlib.h, string.h, math.h, conio.h, …
|
||
├── samples/ # ~100 worked-example programs
|
||
│ ├── stdio/, kernalio/, memmap/, hires/, hiresmc/, particles/, fractals/,
|
||
│ ├── rasterirq/, scrolling/, sprites/, games/
|
||
│ └── resources/ # .bin / .ctm / .spd assets used by samples
|
||
├── autotest/ # Self-test harness
|
||
├── CMakeLists.txt # C++17, globs oscar64/*.cpp
|
||
├── make/ # Makefile-based build (no CMake needed)
|
||
├── oscar64.sln / .vcxproj
|
||
└── oscar64.md # The full reference manual (also in README)
|
||
```
|
||
|
||
Build: `make -C make compiler` (uses the makefile). Or `cmake -B build && cmake --build build` — both produce a single `oscar64` binary. Windows is supported via the included `.sln`.
|
||
|
||
## 3. Compilation pipeline
|
||
|
||
From `oscar64.cpp` (the driver) the lifecycle is short and explicit:
|
||
|
||
```cpp
|
||
Compiler* compiler = new Compiler();
|
||
compiler->ParseSource(); // lex + parse + AST for every translation unit
|
||
compiler->GenerateCode(); // analyze, optimize, code-gen, link
|
||
compiler->WriteOutputFile(...);// emit .prg / .crt / .bin (+ .map / .asm / .int / .lbl)
|
||
```
|
||
|
||
Inside `Compiler::ParseSource()` (`Compiler.cpp:57`):
|
||
|
||
1. Choose zero-page register layout from `-tm` (the 6502 has no general-purpose
|
||
regs, so the compiler dedicates a fixed ZP window per target — see
|
||
`BC_REG_*` symbols).
|
||
2. Loop: pop a pending `CompilationUnit` from the queue, open the file via
|
||
`Preprocessor`, feed it to `Scanner` → `Parser`. `#pragma compile("foo.c")`
|
||
enqueues additional units, which is how the "no makefile" build model works:
|
||
`#include <stdio.h>` literally drags `stdio.c` into the build.
|
||
|
||
Inside `Compiler::GenerateCode()` (`Compiler.cpp:550`):
|
||
|
||
1. Verify the runtime startup (`crt.c`) is present, otherwise error.
|
||
2. Materialize default memory regions for the chosen target machine — e.g. for
|
||
a C64 PRG: `zeropage` at `0xf7..0xff` (or `0x80..0xff` with `-xz`), `startup`
|
||
at `0x801..0x880` (native) or `0x801..0x900` (bytecode), and a `main` region
|
||
that holds `code`, `data`, `bss`, `heap`, `stack`.
|
||
3. Build the vtables (`BuildVTables`), check `operator new` overrides, finish
|
||
deferred template expansion.
|
||
4. Per-procedure: `CompileProcedure()` → `InterCodeGenerator` lowers the AST to
|
||
IC, then for `-n` (native, the default) `NativeCodeGenerator` emits 6502,
|
||
otherwise `ByteCodeGenerator` produces interpreter ops.
|
||
5. `NativeCodeOutliner` (when `-Oo`) collapses repeated code sequences.
|
||
6. `GlobalOptimizer` runs across the whole program: inlining, constant
|
||
propagation, dead-code removal, value-range narrowing, constant-parameter
|
||
folding, recursion/function-pointer detection, etc.
|
||
7. `Linker` lays out objects into regions, produces `.prg` (or `.crt`/`.bin`),
|
||
plus `.map`, `.asm`, `.int`, `.lbl`, and (with `-g`) `.dbj` JSON debug info.
|
||
|
||
The key takeaway: there is no separate `cc1` / `ld` / `as` — it is one
|
||
whole-program compiler. The "linker" is a pass that knows every function and
|
||
chooses what to emit.
|
||
|
||
## 4. How it beats the 6502
|
||
|
||
The reference manual is upfront about the tricks. The four big ones:
|
||
|
||
- **Software stack in zero page.** Locals and parameters live in ZP, not on the
|
||
256-byte hardware stack. The C64 default reserves `0xf7..0xff` for this; with
|
||
`-xz` it expands to `0x80..0xff` (and you lose the `STOP/RESTORE` BASIC return).
|
||
- **Static call-graph stack.** Because the compiler sees the whole program, it
|
||
analyses the call graph and gives every function a fixed ZP frame offset
|
||
instead of pushing/popping. This is why *recursion* and *function pointers*
|
||
are explicitly called out in the manual as "you will pay for this" — both
|
||
defeat the static analysis and force real stack usage.
|
||
- **Zero page as register file.** Globals can be opted into ZP with
|
||
`__zeropage`; the compiler also auto-places small globals into ZP under `-Oz`.
|
||
With `-xz` the wider ZP window gives you ~128 bytes of pseudo-registers.
|
||
- **Value-range analysis.** 16-bit ops on the 6502 are *much* more expensive
|
||
than 8-bit. The compiler narrows arithmetic to 8 bits whenever it can prove
|
||
the value fits. Pointers and globals are harder to narrow (they could change
|
||
behind the compiler's back) so the manual hammers the "use 8-bit locals, mark
|
||
things `const`, prefer `unsigned`" advice.
|
||
|
||
The classic 6502 limitation "no indirect-with-offset" is worked around with
|
||
`__striped` arrays — instead of `LHLHLHLH…` you get `LLLLLLLLHHHHHHHH` so
|
||
each element can be reached with `abs,X` instead of needing a multiply.
|
||
|
||
## 5. The C/C++ language surface
|
||
|
||
C99 is supported in full. C++ mode is selected with `-pp` or a `.cpp` file. The
|
||
listed supported C++ features (from `oscar64.md`) are surprisingly complete:
|
||
|
||
namespaces, references, member functions, ctors/dtors, operator overloading,
|
||
single inheritance, `const` methods, `new`/`delete`/`new[]`/`delete[]`, virtual
|
||
functions, `string`/`iostream`, default params, templates, vector/array/list,
|
||
lambdas, `auto`, range-for, `constexpr`, parameter packs. Not a current
|
||
standard, but plenty for a 6502.
|
||
|
||
## 6. 6502-specific extensions (the fun stuff)
|
||
|
||
These are the bits that make Oscar feel like "C for the C64" rather than
|
||
"hosted C squeezed onto a 6502":
|
||
|
||
- **`#embed`** — the preprocessor can read a binary file, optionally compress
|
||
it (`lzo` / `rle`), slice an offset+length, and emit 8-bit or 16-bit words.
|
||
It also understands Charpad `.ctm` (chars/tiles/map/attr1/attr2) and
|
||
Spritepad `.spd` (sprites/tiles). This is the standard way to ship charset
|
||
and sprite data in a sample.
|
||
- **`#pragma compile("file.c")`** — pulls another translation unit into the
|
||
build. The whole "no makefile" build model rests on this: `#include <stdio.h>`
|
||
ends up `#pragma compile("stdio.c")`-ing the stdio implementation.
|
||
- **Storage-class qualifiers:** `__native` (force native codegen for a fn),
|
||
`__zeropage` (place a global in ZP), `__noinline`, `__forceinline`, `__export`
|
||
(force a symbol to be emitted even if unreferenced), `__striped` (the
|
||
layout trick above), `__memmap` (a stronger memory fence than `volatile`,
|
||
used for things like bank registers that re-map the address space).
|
||
- **Pragmas:** `region(name,start,end,flags,bank,{sections})` and
|
||
`section(name,…)` reshape the memory map. `align(var, 8)`, `heapsize()`,
|
||
`stacksize()`, `overlay(name, bank)`, `unroll(n|full|page)`, `optimize(…)`,
|
||
`reference(name)`, `intrinsic(name)`, `message(...)`, `charmap(char,code,…)`,
|
||
`callinline()`, `warning(disable:num,…)`.
|
||
- **PETSCII and screen-code literals:** `p"hello"` / `P"hello"` makes a
|
||
PETSCII string, `s"hello"` / `S"hello"` makes screen codes. `-psci` flips
|
||
the default for unprefixed strings. `iocharmap(IOCHM_PETSCII_2)` also
|
||
swaps the font.
|
||
- **Inline assembler with full C interop:** `__asm { lda c ; bne w1 ; … }`,
|
||
where `c` is a local var (ZP register) and `accu`/`addr`/`sp`/`fp` are the
|
||
fixed return-value and frame-pointer slots. `Type::Member` (e.g. `lda #P::ns`)
|
||
generates a struct member offset. `__interrupt` saves ZP scratch regs; `__hwinterrupt`
|
||
also saves A/X/Y and exits with `rti`. A top-level `__asm name { … }` defines
|
||
a pure-asm routine. The optimizer (`-Oa`, on at `-O2+`) will rewrite your
|
||
inline asm.
|
||
- **Hypotheticals:** `__assume(x < 10)`, `__assume(p != nullptr)`,
|
||
`__assume(false)` for `default:` arms — these feed the value-range analysis.
|
||
- **Preprocessor loops:** `#assign name expr` and `#repeat … #until cond` let
|
||
you generate unrolled code (e.g. 25 absolute stores to fill a screen
|
||
column). `#for(i, n) text` is the one-liner form.
|
||
|
||
## 7. Memory model and program layout
|
||
|
||
The "Region / Section / Object" three-level linker is worth understanding because
|
||
almost every advanced sample uses it:
|
||
|
||
- **Region** = a physical chunk of memory (or a cartridge bank).
|
||
`#pragma region(main, 0x0a00, 0xa000, , , {code, data, bss, heap, stack})`
|
||
reshapes the default map. Flags and an optional bank slot exist for
|
||
cartridge layouts.
|
||
- **Section** = a logical bucket (`code`, `data`, `bss`, `heap`, `stack`,
|
||
`zeropage`, or a user-named one). `#pragma section(name, 0, startSym, endSym)`
|
||
declares it; the start/end symbols are emitted so asm can refer to them
|
||
(this is how `crt.c` exposes `StackStart`, `StackEnd`, `BSSStart`, `BSSEnd`,
|
||
`CodeStart`, `CodeEnd`).
|
||
- **Object** = one actual blob: a function, a `const` array, a BSS variable,
|
||
a pad, etc. The linker walks the dependency graph from `main` (via the
|
||
startup code in `crt.c`) and only includes reachable objects, which is why
|
||
the whole stdio family can live in a single `.c` without exploding the
|
||
output.
|
||
|
||
For a default C64 PRG the layout is:
|
||
- `0x0801–0x0880` (native) or `0x0801–0x0900` (bytecode) — startup + BASIC stub.
|
||
- `0x0900–0x0a00` — bytecode jump table (only when not all native).
|
||
- `0x0a00–0xa000` — main, with `code`, `data`, `bss`, `heap`, `stack` sections.
|
||
|
||
For cartridges, `easyflash` (`-tf=crt`) expands bank 0 into common memory at
|
||
startup; generic `crt8`/`crt16` use the `rom` region from bank 0 and put
|
||
`bss`/`stack`/`heap` in `main`. The `__bankof(expr)` operator gives you the
|
||
bank id of a symbol, so dynamic code can `mmap_set` to the right bank before
|
||
calling.
|
||
|
||
For larger programs the `#pragma overlay(name, bank)` mechanism turns each
|
||
cartridge bank into a `.prg` file inside the `.d64` image, and `krnio_load`
|
||
brings it in on demand — that's how the games fit.
|
||
|
||
## 8. Targets and output formats
|
||
|
||
`-tm` picks the machine: c64, c128, c128b, c128e, plus4, vic20 (+3/+8/+16/+24),
|
||
pet (8k/16k/32k), nes (nrom, nrom_h, nrom_v, mmc1, mmc3), atari, x16, mega65.
|
||
`-tf` picks the file: `prg`, `crt`, `crt8`, `crt16`, `bin`. `-d64=foo.d64`
|
||
builds a disk image and `-f=path` / `-fz=path` add (optionally compressed)
|
||
resource files to it; `oscar_expand_lzo` decompresses at runtime, and
|
||
`krnio_read_lzo` decompresses on the fly during a kernal read.
|
||
|
||
## 9. Built-in emulator
|
||
|
||
`-e` runs the program in a built-in 6502 emulator, `-ep` profiles it. This is
|
||
the iteration loop: write C, compile, run, no need to launch VICE for the
|
||
common case. (VICE is still the right choice when you need cycle-exact raster
|
||
behaviour — see `-moncommands` for loading the `.lbl` symbol file.)
|
||
|
||
## 10. Useful artifacts the compiler writes
|
||
|
||
Beyond the main binary, you get (all explained in `oscar64.md`):
|
||
|
||
- `.map` — regions, sections, objects, objects-by-size. The first place to
|
||
look when your `.prg` is too big.
|
||
- `.asm` — full 6502 listing with source references when `-g` is on. This is
|
||
what you cross-reference from inside the VICE monitor.
|
||
- `.int` — the intermediate code dump.
|
||
- `.lbl` — `al address .symbol` lines for VICE's monitor.
|
||
- `.dbj` — JSON with `memory`, `variables`, `functions`, `types`. Combined
|
||
with [Modern VICE PDB Monitor](https://github.com/MihaMarkic/modern-vice-pdb-monitor)
|
||
this gives source-level debugging. The manual recommends `-n -g -O0` for
|
||
the best debug experience.
|
||
- `.csz` — every source line annotated with its start address and emitted
|
||
byte count. Great for finding the one loop that ate all your memory.
|
||
|
||
## 11. Optimization levels (cheat sheet)
|
||
|
||
```
|
||
-O0 no optimization
|
||
-O / -O1 default
|
||
-O2 + auto-inline, inline-asm optimizer, constant-parameter folding
|
||
-O3 + auto-place globals in ZP, outliner
|
||
-Os size
|
||
-Oi auto-inline (sub-flag of O2/O3)
|
||
-Oa inline-asm optimizer (sub-flag of O2/O3)
|
||
-Oz globals → zero page (sub-flag of O3)
|
||
-Op constant-parameter folding
|
||
-Oo outliner (extract repeated sequences)
|
||
-Ox pointer-arith page-boundary awareness
|
||
-OM self-modifying code (saves bytes when SMC is cheap)
|
||
```
|
||
|
||
The mantra in the manual: "Avoid recursion. Avoid function pointers. Be aware
|
||
of aliasing. Prefer unsigned. Stick to 8 bits. Prefer enums over #defines. Mark
|
||
const. Use `__assume`." Each of those maps directly to a 6502 pain point.
|
||
|
||
## 12. A minimal end-to-end
|
||
|
||
From `samples/stdio/helloworld.c` (9 lines, the whole file):
|
||
|
||
```c
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
putchar(14); // clear screen
|
||
printf(p"Hello World\n"); // p"" → PETSCII string
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
Build: `oscar64 helloworld.c` → `helloworld.prg`. Run with `x64 helloworld.prg`
|
||
or `oscar64 helloworld.c -e`. The C runtime comes from `include/crt.c` and
|
||
`include/stdio.c`, both pulled in automatically by `#include <stdio.h>` (which
|
||
in turn `#pragma compile("stdio.c")`s the stdio implementation, which
|
||
`#pragma compile("conio.c")`s conio, which is how the whole tree stitches
|
||
together with no makefile).
|
||
|
||
A more interesting example, `samples/hires/lines.c`, shows the full
|
||
hardware-programming idiom: `#include <c64/memmap.h>` to bank out BASIC,
|
||
`#include <c64/vic.h>` to flip into hires mode, `#include <gfx/bitmap.h>` for
|
||
`bm_line`, and the `Bitmap` / `ClipRect` types. Note `char *Hires = (char *)0xe000;`
|
||
— absolute addressing is the normal way to reach video memory.
|
||
|
||
## 13. Things I want to remember when I come back
|
||
|
||
- It is one binary. No assembler, no linker, no CRT to wire up. `crt.c` is
|
||
pulled in by default; `-rt=foo.c` swaps it, `-rt=` removes it.
|
||
- Source files are pulled in by `#pragma compile("x.c")`, not by listing them
|
||
on the command line. The only files on the command line are the user's root
|
||
translation units (and they probably just `#include` headers that do the
|
||
pragma work).
|
||
- The CLI is `-option` style. `-i=path` adds an include, `-ii=path` *replaces*
|
||
the default include, `-tf=…`/`-tm=…` set format/machine, `-dNAME` defines a
|
||
macro, `-O3` is the speed preset, `-e` runs the built-in emulator, `-g`
|
||
emits `.dbj` and source lines in `.asm`.
|
||
- Native code is the default (`-n` is implicit now). `-bc` opts into bytecode.
|
||
- "Region / Section / Object" is the mental model for the linker. Almost every
|
||
non-trivial memory layout in the samples is a `#pragma region(…)`.
|
||
- The two escape hatches when the optimizer is wrong: `__assume(cond)` and
|
||
`#pragma optimize(noasm)` (or `__asm volatile { … }` for a single statement).
|
||
- Read `oscar64.md` in the repo root — it is the real reference manual and is
|
||
far more thorough than what fits here.
|
||
|
||
---
|
||
|
||
# Part II — Programming the C64 with Oscar64
|
||
|
||
This section combines the Oscar64 architecture notes above with the C64
|
||
hardware reference in `../PROG_C64.md` and `../docs/c64/`. The goal is a
|
||
practical "how do I write C for the C64 with this compiler and not
|
||
shoot myself in the foot" guide.
|
||
|
||
## 14. The C64 memory model in Oscar64
|
||
|
||
### 14.1 The default region layout (no `region`/`section` pragmas)
|
||
|
||
The runtime `crt.c` materializes these regions for a C64 PRG (the
|
||
`Compiler::GenerateCode` switch in `Compiler.cpp:550-650` is the
|
||
authoritative source):
|
||
|
||
| Region | Address | Contents |
|
||
|--------|---------|----------|
|
||
| `startup` | $0801-$0880 (native) | BASIC stub, KERNAL trampoline, init code, `main()` |
|
||
| `main` | $0900-$A000 | `code`, `data`, `bss`, `heap`, `stack` sections |
|
||
|
||
This gives you 38 KB of usable RAM at $0900-$A000 once the KERNAL ISR
|
||
trampoline is set up. (For a fresh C64 that's *less* than BASIC sees,
|
||
but in exchange you have the entire 38 KB contiguous, no ROM reloads,
|
||
and a known startup state.)
|
||
|
||
Inside the `main` region, the default stack size is 1 KB and the default
|
||
heap size is 1 KB. Both are tweakable with `#pragma stacksize(N)` and
|
||
`#pragma heapsize(N)`; setting `heapsize(0)` is mandatory if you don't
|
||
use `malloc`.
|
||
|
||
### 14.2 The zeropage software-stack window
|
||
|
||
Oscar64 dedicates a fixed zero-page window per target machine to its
|
||
software stack (parameters, locals, temporaries). For the C64 with
|
||
`-O3` the default window is `0xF7..0xFF` (9 bytes: frame pointer, stack
|
||
pointer, two scratch pairs, plus a return-address register). With
|
||
`-xz` you get `0x80..0xFF` (128 bytes — much more room for locals, but
|
||
the KERNAL's `STOP/RESTORE` warm-start vector at $02/$03 stops working,
|
||
which is why the Oscar64 docs call it "extended zero page usage").
|
||
|
||
This means: by default, you cannot freely use $F7-$FF. The Oscar64
|
||
runtime uses them, and so does its `mmap_set` trampoline. If you write
|
||
inline asm that touches those addresses, you will collide. If you want
|
||
more zero-page scratch you have three options:
|
||
1. Compile with `-xz` to widen the window.
|
||
2. Use `__zeropage` on globals (compiler places them in `$80..$F7` if
|
||
`-Oz` is on, otherwise wherever it fits).
|
||
3. Mark the global `__zeropage int x;` and use the same name in
|
||
inline asm — Oscar64 will route both to the same ZP slot.
|
||
|
||
### 14.3 The bank-switching model
|
||
|
||
The C64's `mmap_set()` API in `c64/memmap.h` writes to `$01` (the
|
||
6510 port) to change which ROMs and I/O are visible. The trampoline
|
||
that `mmap_set` installs at $EA31 (the KERNAL IRQ vector location) is
|
||
key: when an IRQ fires, the trampoline briefly restores the default
|
||
memory configuration, calls the real KERNAL ISR, and then re-applies
|
||
your mapping. Without that trampoline, the KERNAL's IRQ handler
|
||
(which is at $EA31 in KERNAL ROM) would see whatever banking you've
|
||
set up, which will crash because the KERNAL isn't actually there.
|
||
|
||
Practical implications:
|
||
- If you write your own raster IRQ, end with `JMP $EA31` (which goes
|
||
through the trampoline) **not** `JMP kernal_isr` directly, unless
|
||
you have re-implemented the IRQ save/restore yourself.
|
||
- `mmap_trampoline()` is called once at startup and must complete
|
||
successfully before you change the memory configuration. It
|
||
allocates 2 bytes somewhere safe (usually in the stack area).
|
||
|
||
### 14.4 What goes where, end to end
|
||
|
||
| Memory | Used by | Why |
|
||
|--------|---------|-----|
|
||
| $00-$01 | 6510 port (always) | Read/write = port bits, not RAM |
|
||
| $02-$8F | KERNAL/BASIC scratch | Don't touch in your C code |
|
||
| $90-$A2 | KERNAL state (jiffy clock, I/O status) | Use `kernal_*` functions, don't POKE |
|
||
| $C0-$CF | KERNAL screen editor | Use conio.h, don't POKE |
|
||
| $F7-$FF | Oscar64 ZP software stack | Don't touch in inline asm |
|
||
| $0100-$01FF | 6502 hardware stack | Don't store persistent C data here |
|
||
| $0400-$07FF | Screen RAM (40×25) | Use `conio.h` for text I/O |
|
||
| $0801-$0880 | Startup (your code starts here) | Don't try to call into it |
|
||
| $0900-$A000 | Your code, data, bss, heap, stack | Where the linker puts your stuff |
|
||
| $A000-$BFFF | BASIC ROM (default) or your RAM (`-O3` banks it out) | Bank-switch with `mmap_set` |
|
||
| $C000-$CFFF | Always free for your code | Bank-switch with `mmap_set` |
|
||
| $D000-$DFFF | I/O + Color RAM (CHAR ROM if CHAREN=0) | Use the `c64/*.h` headers |
|
||
| $D800-$DBFF | Color RAM (low 4 bits only) | `screen.color[i] = c;` style helpers |
|
||
| $E000-$FFFF | KERNAL ROM (default) or your RAM | Bank-switch with `mmap_set` |
|
||
|
||
## 15. How Oscar64 helps — and what it doesn't
|
||
|
||
This is the section that says "you used to have to do this in assembly,
|
||
now you can do it in C, but you still have to know about the hardware
|
||
because the compiler won't do the unsafe thing for you."
|
||
|
||
### 15.1 The wins
|
||
|
||
- **Replacing hand-rolled 6502 ASM.** Things like the dhrystone
|
||
benchmark at 442 iterations/sec (cited in the Oscar64 README) used
|
||
to require an expert ASM programmer. With Oscar64 you can express
|
||
the same logic in C, write `__assume` hints, and let the optimizer
|
||
produce near-optimal code. The oscar64 manual has a worked example
|
||
where a `Plot()` function written in C and marked `__native` ends
|
||
up exactly as good as hand-written ASM.
|
||
|
||
- **No more "this is a global, so it's worse" pessimization.** Static
|
||
call-graph analysis means Oscar64 gives every function a fixed ZP
|
||
frame offset for its parameters and locals. You can pass ints to
|
||
functions and Oscar64 will try to use absolute addressing when it
|
||
can prove the value is in the ZP-resident static frame.
|
||
|
||
- **Striped arrays for the 6502 no-indirect-with-offset problem.** The
|
||
`__striped` qualifier is the killer feature for hires graphics. A
|
||
320-byte bitmap with `__striped` becomes `LLLLLLLLHHHHHHHH` so the
|
||
hot inner loop compiles to `LDA $xxxx,Y` instead of needing a
|
||
multiply.
|
||
|
||
- **PETSCII/screen-code literals.** `printf(p"Hello\n")` and the
|
||
`p""` / `s""` prefixes, plus `iocharmap(IOCHM_PETSCII_2)`, mean you
|
||
don't have to write a translation table for every string. The
|
||
compiler emits the correct byte sequence for the active character
|
||
set at compile time.
|
||
|
||
- **Inline asm with full C interop.** When you do need to drop to
|
||
ASM, you can write `__asm { lda c ; bne w1 ; … }` and the
|
||
compiler will substitute `c` with its ZP location, set up the
|
||
right addressing mode, and let the optimizer clean up afterwards
|
||
(when `-Oa` is on, which it is for `-O2+`).
|
||
|
||
- **Compile-time `#embed` for binary data.** No more `FOR I=0 TO 62:
|
||
READ B: POKE 12288+I,B: NEXT I` in BASIC. The `#embed` directive
|
||
pulls in a `.bin`, `.ctm`, or `.spd` file at compile time, with
|
||
optional LZO or RLE compression, and emits the bytes as a `const`
|
||
array you can reference directly.
|
||
|
||
- **`#pragma compile("foo.c")` means no makefile.** The entire stdio,
|
||
conio, gfx, and c64/ families of headers pull in their
|
||
implementations this way. You write one `main.c`, `#include
|
||
<stdio.h>`, and the compiler drags in `stdio.c` and `conio.c` and
|
||
`crt.c` automatically.
|
||
|
||
- **Whole-program dead-code elimination.** `printf("hello\n")`
|
||
pulls in only the code path that formats a string and calls
|
||
`CHROUT`. The floating-point formatter, the file I/O, the scanf
|
||
family — all of it disappears unless you actually use it. With
|
||
`-dNOFLOAT` you can also strip float support from printf entirely.
|
||
|
||
- **Automatic zero-page placement under `-Oz`.** Globals and small
|
||
arrays that the optimizer can prove are hot get moved into ZP at
|
||
`-O3 + -Oz`. You don't have to manually mark every variable; the
|
||
compiler does it for you.
|
||
|
||
- **Disk overlays via `-tf=d64`.** Banked code becomes a `.prg` per
|
||
bank inside the disk image. You can write your C code as if it
|
||
were all linked, and the compiler figures out which bank each
|
||
function goes into.
|
||
|
||
- **The built-in emulator (`-e`, `-ep`).** Edit, compile, run, watch
|
||
the screen. No need to launch VICE for the iteration loop. (VICE
|
||
is still the right choice for cycle-exact raster work.)
|
||
|
||
### 15.2 The "you still need to know the hardware" cases
|
||
|
||
- **The badline penalty.** A `for` loop that runs across a badline
|
||
boundary will take 40 extra cycles on that one line. The compiler
|
||
doesn't know about badlines. If you have a tight loop that needs
|
||
to be cycle-stable, put it all inside one raster line (a "stable
|
||
raster" loop, commonly on line 311 on PAL) or you'll see glitches.
|
||
Workaround: use a raster IRQ to start the work right after a known
|
||
stable line.
|
||
|
||
- **VIC bank bits.** The VIC only has a 14-bit address bus. The
|
||
other 2 bits come from CIA 2 PRA. Oscar64's `screen` / `bitmap`
|
||
helpers manage this for you, but if you write inline asm that
|
||
reads video memory at a literal address, you have to make sure
|
||
the right bank is selected. The right thing is to set up a
|
||
`char * const Hires = (char *)0xe000;` in C — Oscar64 will emit
|
||
the right bank-switch dance for you the first time you read or
|
||
write through that pointer (or at startup, depending on the
|
||
helper).
|
||
|
||
- **Color RAM upper nibble.** The compiler can't help you if you
|
||
`*((char *)0xd800) = 0xFF;` and expect the high nibble to be 0.
|
||
It won't be. Use `screen.color[i] = c;` or `POKE_COLOR(0xd800, i,
|
||
c)` style helpers instead.
|
||
|
||
- **Sprite pointers need 64-byte alignment.** Oscar64's `Sprite` type
|
||
in `c64/sprites.h` handles this for you (it pads the structure to
|
||
64 bytes), but if you declare `char my_sprite[63]` and `POKE 2040,
|
||
(int)my_sprite/64;`, you'll crash if `my_sprite` isn't aligned.
|
||
Use the helpers.
|
||
|
||
- **PETSCII vs ASCII.** Writing `"Hello\n"` in C gives you ASCII
|
||
bytes 48, 65, 6C, 6C, 6F, 0A. The C64's character ROM has `H` at
|
||
PETSCII 8, not 65, and uses CR (0x0D) instead of LF (0x0A). Use
|
||
the `p""` prefix in Oscar64 or the compiler flag `-psci`, or your
|
||
text will look like garbage.
|
||
|
||
- **KERNAL save/restore around the trampoline.** If you call into
|
||
the KERNAL directly (e.g. `JSR $FFD2` to print a character), the
|
||
trampoline will fix up the memory configuration for you. If you
|
||
jump to a KERNAL function in the middle (e.g. setting up your own
|
||
routine that ends with `JMP $EA81`), the KERNAL will read its own
|
||
data at $EA31 and find your trampoline instead — which is fine if
|
||
the trampoline is installed, but if you've done a `mmap_set` that
|
||
banks out KERNAL, you'll crash. The fix is to call
|
||
`mmap_set(MMAP_ROM)` first, *then* do your JMP.
|
||
|
||
- **Cycle-counting is your job.** Oscar64 will tell you the cycle
|
||
count of the code it generated (look at the `.asm` file), but it
|
||
won't tell you that the work happens during a badline and is
|
||
therefore going to be 40 cycles slower. If you're trying to fit
|
||
code in a specific raster window, you still need to read the
|
||
Christian Bauer VIC article.
|
||
|
||
- **The CIA 1 IRQ is the system's IRQ.** If you set up a raster IRQ
|
||
and forget to mask CIA 1's timer A in `$DC0D`, you'll get a
|
||
nested IRQ every jiffy (1/60 sec) and your raster code will
|
||
glitch. The Oscar64 `rasterirq.h` library handles this for you;
|
||
the pattern is always:
|
||
```c
|
||
SEI;
|
||
POKE(0xDC0D, 0x7F); // mask CIA 1 IRQs
|
||
POKE(0xDD0D, 0x7F); // mask CIA 2 IRQs
|
||
POKE(0xD01A, 0x01); // enable VIC raster IRQ
|
||
CLI;
|
||
```
|
||
|
||
- **Self-modifying code is sometimes required.** The 6502 has no
|
||
indirect-with-offset addressing for arrays, so a "load a byte from
|
||
a C array" can take more cycles than a "load a byte from a known
|
||
constant address". Sometimes the right answer is `__asm { lda
|
||
my_const+1 }` after the compiler has put `my_const` in a known
|
||
place. Oscar64 supports this with `#pragma optimize(asm)` to
|
||
allow the optimizer to clean it up, or `__asm volatile { … }` if
|
||
you want it untouched.
|
||
|
||
## 16. Oscar64 patterns for the C64 (a tour)
|
||
|
||
### 16.1 Hires frame setup, the idiomatic way
|
||
|
||
```c
|
||
#include <c64/memmap.h>
|
||
#include <c64/vic.h>
|
||
#include <gfx/bitmap.h>
|
||
|
||
char Color[1000];
|
||
char Hires[8000];
|
||
Bitmap Screen;
|
||
|
||
int main(void) {
|
||
mmap_trampoline();
|
||
mmap_set(MMAP_RAM); // bank out BASIC + KERNAL → 38 KB free
|
||
mmap_set(MMAP_NO_ROM); // also bank out CHAR ROM at $D000
|
||
|
||
memset(Color, 1, 1000); // white on black
|
||
memset(Hires, 0, 8000); // clear bitmap
|
||
vic_setmode(VICM_HIRES, Color, Hires);
|
||
vic.color_border = VCOL_WHITE;
|
||
bm_init(&Screen, Hires, 40, 25);
|
||
|
||
// ... draw stuff ...
|
||
getch();
|
||
|
||
mmap_set(MMAP_ROM); // restore for clean exit
|
||
vic_setmode(VICM_TEXT, (char *)0x0400, (char *)0x1000);
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
Compare to the equivalent in raw ASM:
|
||
- Set `$01` to `$34` (bank out BASIC+KERNAL+CHAREN).
|
||
- Wait for a stable raster line.
|
||
- Write `$D011` for ECM=BMM=0, DEN=1, RSEL=1.
|
||
- Write `$D016` for MCM=0, CSEL=1.
|
||
- Write `$D018` to set the screen memory and bitmap base.
|
||
- Set CIA 2 PRA bits 0-1 to the right VIC bank.
|
||
- Set up VIC bank by writing to `$DD00`.
|
||
- Clear 8 KB of bitmap and 1 KB of color RAM.
|
||
- ... draw ...
|
||
- Reverse everything.
|
||
|
||
The C version is 5 lines of setup. The ASM version is 30+.
|
||
|
||
### 16.2 Sprites the right way
|
||
|
||
```c
|
||
#include <c64/sprites.h>
|
||
|
||
// Sprite data is 64 bytes (63 + 1 pad) per sprite
|
||
const char MySprite[64] = { 0, 126, 0, 3, 255, 192, /* ... */ };
|
||
|
||
int main(void) {
|
||
// ... init ...
|
||
spr_init(SPRITES_BASE); // sets up sprite pointers
|
||
spr_set(0, MySprite, 100, 100, VCOL_WHITE);
|
||
spr_show(0, true);
|
||
// ...
|
||
}
|
||
```
|
||
|
||
In `c64/sprites.h`, `spr_set` does the math (`(int)MySprite >> 6` for
|
||
the pointer, separate X/Y writes, color register write). The
|
||
`Sprite[8]` and `SpritePointer[8]` types handle the "where do I put
|
||
sprite data" question (must be 64-byte aligned in the current VIC
|
||
bank; the helper reserves a 512-byte region and aligns each sprite
|
||
inside it).
|
||
|
||
### 16.3 Raster IRQ, the right way
|
||
|
||
```c
|
||
#include <c64/rasterirq.h>
|
||
|
||
RIRQCode screenRIRQ;
|
||
|
||
void __interrupt screen_handler(void) {
|
||
// ... do per-line work ...
|
||
rasterirq_next(&screenRIRQ, line + 8); // schedule next IRQ
|
||
}
|
||
|
||
int main(void) {
|
||
// ...
|
||
rasterirq_init();
|
||
screenRIRQ.handler = screen_handler;
|
||
rasterirq_setup(&screenRIRQ, 100, 0); // line 100, no change
|
||
rasterirq_enable(&screenRIRQ);
|
||
rasterirq_sort(); // sort all active RIRQs
|
||
// ... main loop ...
|
||
}
|
||
```
|
||
|
||
This wraps all the `SEI/CLI/POKE $D01A/$0314/$0315` dance and also
|
||
installs a KERNAL trampoline that preserves the jiffy clock and
|
||
keyboard scan. Without `rasterirq.h` you'd write 30+ lines of
|
||
assembly and 2 separate hand-managed chains of ISRs.
|
||
|
||
### 16.4 The KERNAL ISR trick
|
||
|
||
If you want to keep the KERNAL's jiffy clock, keyboard scan, and
|
||
cursor blink going while running your own raster code, end your ISR
|
||
with `JMP $EA31` (not $EA81). The KERNAL ISR at $EA31 will run after
|
||
yours and do the standard work. The Oscar64 `rasterirq.h` library
|
||
uses this approach for all its "system-friendly" RIRQs.
|
||
|
||
If you want the IRQ to be as fast as possible and don't care about
|
||
the jiffy clock, end with `JMP $EA81` (the register-restore stub).
|
||
The KERNAL still restores A/X/Y and RTIs, but doesn't do the
|
||
service work.
|
||
|
||
If you want the IRQ to do absolutely nothing but return (e.g. a
|
||
timer-based music player that runs as fast as possible), you can
|
||
write your own `RTI` and skip the KERNAL entirely. But you must
|
||
preserve the stack layout (PC_hi, PC_lo, P, A, X, Y from top to
|
||
bottom) so the RTI returns to the right place.
|
||
|
||
### 16.5 The "what about the 6510 port?" question
|
||
|
||
The C64's bank switching is just `*((char *)0x01) = value;`. The
|
||
Oscar64 `mmap_set` API wraps this but also installs the trampoline.
|
||
If you need to do it yourself (rare), the pattern is:
|
||
|
||
```c
|
||
__asm {
|
||
sei
|
||
lda #0x34
|
||
sta $01 ; bank out BASIC + KERNAL
|
||
jsr $02 ; call user code (at $02-$03 trampoline slot)
|
||
lda #0x37
|
||
sta $01 ; restore default
|
||
cli
|
||
}
|
||
```
|
||
|
||
The `jsr $02` is the key. It's not a real "jsr"; it's a trampoline
|
||
slot that the user can install code at. The mmap library uses this
|
||
to call user code with the ROMs banked out.
|
||
|
||
### 16.6 Using `__assume` for the C64
|
||
|
||
The Oscar64 optimizer is value-range-aware. It will use 8-bit
|
||
operations whenever it can prove the value fits in 8 bits. The
|
||
classic case on the C64 is VIC register writes: $D011, $D016, $D018
|
||
all only need the low 5-8 bits, so an `int` value with the high bits
|
||
clobbered is wasteful.
|
||
|
||
```c
|
||
void set_scroll(char x, char y) {
|
||
// x is 0-7, y is 0-7 — known at the call site
|
||
__assume(x < 8);
|
||
__assume(y < 8);
|
||
*((char *)0xD016) = (*((char *)0xD016) & 0xF8) | x;
|
||
*((char *)0xD011) = (*((char *)0xD011) & 0xF8) | y;
|
||
}
|
||
```
|
||
|
||
The `__assume`s tell the compiler that `x` and `y` fit in 3 bits,
|
||
so it can AND with 7 instead of $F8 and then OR, saving 4 cycles
|
||
per call. For a function called in a raster IRQ this matters.
|
||
|
||
### 16.7 `__striped` for the bitmap
|
||
|
||
The classic demo-scene optimization. Without `__striped`, accessing
|
||
`bitmap[y][x]` (a `char [200][40]`) needs a multiply-by-40 to get the
|
||
byte offset. With `__striped`, the compiler splits the array into
|
||
separate per-row arrays that are 40 bytes wide each, and uses `LDA
|
||
$xxxx,X` (no multiply) to read them.
|
||
|
||
```c
|
||
__striped char hires[8000]; // interleaved as 8 1000-byte stripes
|
||
// wait, this isn't quite right; see below
|
||
```
|
||
|
||
Actually for `Bitmap` the trick is more subtle. The Oscar64
|
||
`gfx/bitmap.h` library uses `__striped` internally to get good code
|
||
for the per-pixel access. The `bm_line` and `bm_fill` functions are
|
||
written in a way that lets the compiler emit very tight code for the
|
||
common case of "plot a pixel in a 320×200 bitmap".
|
||
|
||
## 17. C64-specific optimization notes
|
||
|
||
### 17.1 Use `unsigned char` for VIC coordinates
|
||
|
||
A sprite X position is 0-344 (with bit 8 in $D010). A sprite Y
|
||
position is 0-255. Color register values are 0-15. Background color
|
||
values are 0-15. The Oscar64 optimizer will use 8-bit operations
|
||
when it can prove the value fits in 8 bits; if you use `int` for a
|
||
"the value is always 0-15" variable, the optimizer may not be able
|
||
to prove it, and you'll get 16-bit code for a 4-bit value.
|
||
|
||
### 17.2 Use `const` aggressively
|
||
|
||
A `const` global is read-only, so the optimizer can keep it in the
|
||
char ROM or in a register, not in RAM. A `const` array of 256
|
||
bytes (like a color lookup table) can be in the char ROM at no cost.
|
||
A non-const array of 256 bytes costs 256 bytes of RAM.
|
||
|
||
### 17.3 Use `enum` instead of `#define` for register values
|
||
|
||
```c
|
||
enum VICMode {
|
||
VICM_TEXT = 0x00,
|
||
VICM_HIRES = 0x20,
|
||
VICM_MULTI = 0x10,
|
||
VICM_EXTENDED = 0x40,
|
||
VICM_BITMAP = 0x20,
|
||
// ...
|
||
};
|
||
```
|
||
|
||
vs
|
||
|
||
```c
|
||
#define VICM_HIRES 0x20
|
||
```
|
||
|
||
The `enum` version lets the optimizer prove that `mode` is a small
|
||
value, so the write to $D011 can be an 8-bit operation. The
|
||
`#define` version is a preprocessor substitution, so the optimizer
|
||
sees `int mode = 0x20;` and can't narrow it.
|
||
|
||
### 17.4 Use `__forceinline` for tiny helpers in hot paths
|
||
|
||
If you have a `bm_pixel()` function that's called millions of times
|
||
per frame, mark it `__forceinline` (or at least `inline`). The
|
||
`__striped` access inside will then compile to a single `LDA
|
||
$xxxx,X` instruction in the caller's body.
|
||
|
||
### 17.5 Use `__noinline` for cold paths
|
||
|
||
Conversely, if a function is only called in a startup path or an
|
||
error path, mark it `__noinline`. The optimizer will keep it
|
||
out-of-line, saving code size in the hot paths.
|
||
|
||
### 17.6 `__hwinterrupt` for ISRs
|
||
|
||
```c
|
||
__hwinterrupt void my_isr(void) {
|
||
// saves A/X/Y, your code, then RTI
|
||
vic.color_border ^= 1;
|
||
}
|
||
```
|
||
|
||
`__hwinterrupt` saves the CPU registers for you, runs your code,
|
||
then exits with `RTI`. This is the right annotation for an ISR that
|
||
takes over the IRQ line completely (e.g. a music player that
|
||
disables the KERNAL ISR).
|
||
|
||
`__interrupt` saves just the zero-page registers; you'd use this
|
||
for a function that calls back into C and needs the ZP frame to be
|
||
preserved but doesn't touch the CPU registers.
|
||
|
||
For most C64 work, `__hwinterrupt` is what you want. The Oscar64
|
||
runtime's `rasterirq.h` uses it.
|
||
|
||
### 17.7 `#pragma optimize(noasm)` for fragile inline asm
|
||
|
||
If you have an inline asm snippet that you're absolutely sure is
|
||
cycle-perfect and you don't want the optimizer to touch it, wrap
|
||
it in `#pragma optimize(noasm) ... #pragma optimize(asm)` or use
|
||
`__asm volatile { ... }`. The Oscar64 `-Oa` flag (on by default at
|
||
`-O2+`) will rewrite your inline asm — usually correctly, but
|
||
occasionally in surprising ways.
|
||
|
||
## 18. The "common beginner mistakes" list
|
||
|
||
These are mistakes that the C64 + Oscar64 combination makes easy and
|
||
that cost you a frame or a crash.
|
||
|
||
1. **Forgetting `mmap_trampoline()`.** Symptom: works in the
|
||
emulator, crashes on real hardware. Cause: the KERNAL ISR doesn't
|
||
see the KERNAL ROM and tries to execute garbage.
|
||
|
||
2. **Forgetting to mask CIA 1 in a raster IRQ.** Symptom: works
|
||
most of the time, but every 1/60 sec the IRQ fires twice
|
||
(once for your raster, once for the jiffy clock) and the second
|
||
one is the KERNAL ISR. Cause: the CIA 1 timer A IRQ is enabled
|
||
by default. Fix: `POKE(0xDC0D, 0x7F)` at the start of your IRQ
|
||
setup.
|
||
|
||
3. **Reading from `*((char *)0xD800)` and expecting a meaningful
|
||
upper nibble.** Symptom: weird color values. Cause: only the
|
||
lower 4 bits are connected.
|
||
|
||
4. **Writing `printf("Hello\n")` and getting garbage.** Symptom:
|
||
text doesn't display correctly. Cause: PETSCII vs ASCII. Fix:
|
||
use `p""` prefix or `-psci` flag.
|
||
|
||
5. **Setting a sprite pointer to an unaligned address.** Symptom:
|
||
the sprite shows as garbage or as the wrong sprite. Cause: the
|
||
pointer value is the pattern address divided by 64, so the
|
||
pattern must be 64-byte aligned. Fix: use the `Sprite` /
|
||
`spr_set` helpers from `c64/sprites.h`.
|
||
|
||
6. **Calling `mmap_set` from inside an ISR.** Symptom: crash.
|
||
Cause: `mmap_set` modifies $01, and if the KERNAL trampoline
|
||
isn't installed yet, or if you re-enter the trampoline, things
|
||
go wrong. Fix: do all your mmap changes at startup, not in an
|
||
ISR.
|
||
|
||
7. **Forgetting to restore the IRQ vector.** Symptom: when your
|
||
program exits, the machine hangs or crashes. Cause: the IRQ
|
||
vector at $0314 still points to your ISR, which is no longer
|
||
there. Fix: `POKE(0x0314, 0x31); POKE(0x0315, 0xEA);` before
|
||
exit (the default KERNAL vector).
|
||
|
||
8. **Using `char` and expecting it to be 8 bits unsigned.** It's
|
||
not — C `char` is signed by default on the C64 with the
|
||
`signed char` type and `unsigned char` for unsigned. For VIC
|
||
color values, sprite coordinates, and bitmap data, use
|
||
`unsigned char` explicitly. The `oscar64` `byte` typedef is
|
||
`unsigned char`.
|
||
|
||
9. **Forgetting `-rt=` when you want a no-runtime build.** The
|
||
default `crt.c` includes BASIC + KERNAL. If you want a
|
||
self-contained 8 KB binary that boots without the KERNAL,
|
||
pass `-rt=my_crt.c` (or `-rt=` for no runtime at all and you
|
||
supply every byte yourself).
|
||
|
||
10. **Using `malloc` without checking the heap size.** Default
|
||
heap is 1 KB. If you allocate more than that, you corrupt
|
||
the stack. Fix: `#pragma heapsize(N)` to set the heap, or
|
||
use a memory pool you manage yourself.
|
||
|
||
## 19. The mental checklist before you ship
|
||
|
||
For a C64 release, the things I want to verify:
|
||
|
||
- [ ] Does it boot on real hardware? (Emulators are forgiving; the
|
||
real chip is not.)
|
||
- [ ] Does the raster IRQ work at NTSC frame rate too? (Many C64
|
||
demos were PAL-only. PAL has 312 lines, NTSC has 263.)
|
||
- [ ] Does it survive RESTORE? (RUN/STOP+RESTORE should return to
|
||
BASIC, not hang.)
|
||
- [ ] Does it survive a disk change? (The serial bus IRQ will fire
|
||
when the drive door is toggled.)
|
||
- [ ] Does the .prg file size match what the user can load? (A
|
||
stock C64 with a stock 1541 can load 202 blocks max; 1
|
||
block = 254 bytes, so ~50 KB is the limit before you need
|
||
a fast loader or multiple disk sides.)
|
||
- [ ] Does the code size fit in $0900-$A000? (38 KB is the
|
||
default, but your code + data + bss + heap + stack all have
|
||
to fit.)
|
||
- [ ] If you used `mmap_set(MMAP_RAM)`, did you `mmap_set(MMAP_ROM)`
|
||
before exiting? (Otherwise the next program loaded sees a
|
||
weird memory state.)
|
||
- [ ] If you wrote your own ISR, did you ack the IRQ in `$D019`?
|
||
(Otherwise the IRQ fires forever.)
|
||
- [ ] Did you mask the CIAs you don't use? (An unmasked CIA 1
|
||
timer A will fire every jiffy, even if you don't care.)
|
||
- [ ] Are the sprites' 64-byte alignment OK? (Use the helpers,
|
||
or check with `assert(((int)my_sprite & 63) == 0);`)
|
||
- [ ] Are the color values 0-15? (The low 4 bits only. The high
|
||
4 bits of $D021-$D02E are unused.)
|
||
- [ ] Does the code work with -O0, -O1, -O2, and -O3? (Each level
|
||
may surface a different bug; -O0 hides aliasing problems
|
||
that -O3 exploits.)
|
||
- [ ] If you used `__assume`, is it true? (A wrong `__assume` is a
|
||
silent bug. Test it at -O0 too.)
|
||
|
||
The Oscar64 compiler gives you an enormous productivity boost over raw
|
||
assembly for the C64 — you can write 10x as much code in the same
|
||
time, and most of it will be near-optimal out of the box. The cost
|
||
is that you have to understand the hardware enough to write the
|
||
right C, and the Oscar64 runtime headers and pragmas give you most
|
||
of what you need. The remaining 10% — the cycle-perfect raster
|
||
tricks, the badline-aware work, the disk-overlayed banked code —
|
||
still requires reading the Christian Bauer VIC article and the
|
||
comprehensive reference in `../PROG_C64.md`.
|
||
</content>
|
||
</invoke> |