4

I'm developing a game for C64 using CC65 to compile my C code to ASM. To get started quickly, I have been defining the data for my sprites in C arrays and then copying the data in those arrays to the correct location in memory for my sprites to point to. I have pointed the VIC II to Bank 3 and have my sprite data starting at 0xE000. This has been working fine but obviously that means my sprite data is stored in two places and that is not ideal.

So I want to load the data directly to 0xE000 at compile time using a segment in an ASM file like this:

.segment "GFXDATA"

.byte $7f,$ff,$fe,$e0,$00,$01,$e0,$00
.byte $01,$e0,$00,$01,$e0,$00,$01,$e0
.byte $00,$01,$e0,$c0,$c1,$e1,$21,$21
.byte $e0,$00,$01,$e0,$00,$01,$e0,$00
.byte $01,$e0,$3f,$01,$e0,$40,$81,$e0
.byte $80,$41,$e1,$00,$21,$e0,$00,$01
.byte $e0,$00,$01,$e0,$00,$01,$ff,$ff
.byte $ff,$ff,$ff,$ff,$7f,$ff,$fe,$01

To start with I created a simple test program that doesn't switch VIC Banks and loading the sprite data at 0x0840. This works fine after I modified the CC65 config file to look like this:

FEATURES {
    STARTADDRESS: default = $0801;
}
SYMBOLS {
    __LOADADDR__:  type = import;
    __EXEHDR__:    type = import;
    __STACKSIZE__: type = weak, value = $0800; # 2k stack
    __HIMEM__:     type = weak, value = $D000;
}
MEMORY {
    ZP:       file = "", define = yes, start = $0002,           size = $001A;
    LOADADDR: file = %O,               start = %S - 2,          size = $0002;
    HEADER:   file = %O, define = yes, start = %S,              size = $000D;
    MAIN:     file = %O, define = yes, start = __HEADER_LAST__, size = __HIMEM__ - __HEADER_LAST__;
    BSS:      file = "",               start = __ONCE_RUN__,    size = __HIMEM__ - __STACKSIZE__ - __ONCE_RUN__;
}
SEGMENTS {
    ZEROPAGE: load = ZP,       type = zp;
    LOADADDR: load = LOADADDR, type = ro;
    EXEHDR:   load = HEADER,   type = ro;
    STARTUP:  load = MAIN,     type = ro;
    GFXDATA:  load = MAIN,     type = ro;
    LOWCODE:  load = MAIN,     type = ro,  optional = yes;
    CODE:     load = MAIN,     type = ro;
    RODATA:   load = MAIN,     type = ro;
    DATA:     load = MAIN,     type = rw;
    INIT:     load = MAIN,     type = rw;
    ONCE:     load = MAIN,     type = ro,  define   = yes;
    BSS:      load = BSS,      type = bss, define   = yes;
}
FEATURES {
    CONDES: type    = constructor,
            label   = __CONSTRUCTOR_TABLE__,
            count   = __CONSTRUCTOR_COUNT__,
            segment = ONCE;
    CONDES: type    = destructor,
            label   = __DESTRUCTOR_TABLE__,
            count   = __DESTRUCTOR_COUNT__,
            segment = RODATA;
    CONDES: type    = interruptor,
            label   = __INTERRUPTOR_TABLE__,
            count   = __INTERRUPTOR_COUNT__,
            segment = RODATA,
            import  = __CALLIRQ__;
}

The only line I added was this one:

GFXDATA:  load = MAIN,     type = ro;

That all works as expected.

So my next test was to try and get the sprite data in Bank 3 like in my game. This is where it has come unstuck. I successfully point the VIC to Bank 3, but my sprites are completely blank. The data doesn't seem to be loaded properly now. Here is the config file I have created to try and get this to work:

FEATURES {
    STARTADDRESS: default = $0801;
}
SYMBOLS {
    __LOADADDR__:  type = import;
    __EXEHDR__:    type = import;
    __STACKSIZE__: type = weak, value = $0800; # 2k stack
    __HIMEM__:     type = weak, value = $D000;
}
MEMORY {
    ZP:       file = "", define = yes, start = $0002,           size = $001A;
    LOADADDR: file = %O,               start = %S - 2,          size = $0002;
    HEADER:   file = %O, define = yes, start = %S,              size = $000D;
    MAIN:     file = %O, define = yes, start = __HEADER_LAST__, size = __HIMEM__ - __HEADER_LAST__;
    BSS:      file = "",               start = __ONCE_RUN__,    size = __HIMEM__ - __STACKSIZE__ - __ONCE_RUN__;
    GFXDATA:  start = $E000, size = $0040, file = %O, define = yes;
}
SEGMENTS {
    ZEROPAGE: load = ZP,       type = zp;
    LOADADDR: load = LOADADDR, type = ro;
    EXEHDR:   load = HEADER,   type = ro;
    STARTUP:  load = MAIN,     type = ro;
    LOWCODE:  load = MAIN,     type = ro,  optional = yes;
    CODE:     load = MAIN,     type = ro;
    RODATA:   load = MAIN,     type = ro;
    DATA:     load = MAIN,     type = rw;
    INIT:     load = MAIN,     type = rw;
    ONCE:     load = MAIN,     type = ro,  define   = yes;
    BSS:      load = BSS,      type = bss, define   = yes;
    GFXDATA:  load = GFXDATA,  type = ro;
}
FEATURES {
    CONDES: type    = constructor,
            label   = __CONSTRUCTOR_TABLE__,
            count   = __CONSTRUCTOR_COUNT__,
            segment = ONCE;
    CONDES: type    = destructor,
            label   = __DESTRUCTOR_TABLE__,
            count   = __DESTRUCTOR_COUNT__,
            segment = RODATA;
    CONDES: type    = interruptor,
            label   = __INTERRUPTOR_TABLE__,
            count   = __INTERRUPTOR_COUNT__,
            segment = RODATA,
            import  = __CALLIRQ__;
}

The main different here is that I am trying to define a new memory area outside of MAIN. One called GFXDATA that starts where my sprites would start at 0xE000. Then I am loading the segment into that.

I must admit I am a pixel artist and game designer first, and a coder second, so I may be missing some really important concepts here.

Any help is much appreciated!

5
  • Some tools are perfectly suited to their task and some just seem to get in the way. A 6502 C compiler always seems to end up feeling like the latter to me. Just one opinion... – Brian H Oct 27 '20 at 13:58
  • @BrianH The fault isn't the compiler here but the rather simple loader the bare C64 provides. And while I in no way endorse C, I must admit that cc65 did a fairly good job putting it to 6502 in general. – Raffzahn Oct 27 '20 at 16:06
  • @Raffzahn my solution has been to feed my "sparse" binaries from the assembler to pucrunch. – Brian H Oct 27 '20 at 17:40
  • Well, yeah, @BrianH , but isn't that as well using a special loader to get it done? This time with a less custom, more brute force algorithm. – Raffzahn Oct 27 '20 at 17:49
  • @Raffzahn Yes. The advantage is the relative simplicity/transparency of this tooling vs. some other, IMO. And since I'm the one using the tooling, my opinion rules ;) – Brian H Oct 27 '20 at 18:10
0

(This seems like an essential duplicate of this question: "Putting code into two different memory areas with cc65/ca65")

Short answer:

CC65 is a compiler, not a loader - and the C64 does only provide a minimal loader moving one continuous chunk of data to a single address. You may need your own loading scheme, or a more sophisticated loader.


All the structural information given (*1) is used to define base values for the generated code. The binary will still simply contain all segments consecutively. It still needs a loader to move the data blocks to each destination address. CC65 as a generic/multi platform tool does not provide one, as that is a machine specific function.

Simple programs are an exception, as they do not need any segment moving. So either you stay with your original version (and reuse the area were the sprites are delivered by the C64 loader) or you write a custom loader - eventually based on the segment data generated by the compiler. See this answer for some additional information.


*1 - I like the detailed structuring, serious, but in case of learning a new tool it might be helpful to cut it down to minimum.

2
  • 1
    Thank you, this helps to understand it a lot better. For now I will continue to use the C arrays containing the data, and live with the data being in two places in memory. For a future project I may investigate creating a custom loader to do as you describe. – monteboyd Oct 27 '20 at 20:31
  • You do not need to keep them twice, use the loading area (where you copy it from) simply for variable storage during program run. Using redefinition in Assembly or a union in C. Then again, I do not think you will run out of memory fast on the C64, or do you? – Raffzahn Oct 27 '20 at 21:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.