Going to commandeer this old thread and use it as a general buildfile guide. Until the information is assimilated into the guide, the contents of the old thread will be at the end of this post.
If you talk to me enough you’ll find that I talk about
buildfiles a lot. The word
buildfile covers both a single file, sometimes called the
build script, and a project organizational structure called the
I want the primary goal of this to be an introduction to (FE5/SNES) buildfiles, with secondary objectives being to lay out some tips and tricks, to show how you maintain and add to a buildfile, and how to package things for others to use.
This is very WIP and will likely remain that way for a long time. I’d really like some feedback here, if you happen to give it a read. Thanks!
Hacking is hard. Names are hard. I like to say these things all of the time.
Organization is hard, too. That’s mostly the subject of this thread. This thread is a bunch of musing about a module/installer format for FE5/SNES(FE?) assembly, tables, etc.
The format for all of this will be for the assembler 64tass.
There are a few 64tass features that are especially important:
This represents 64tass’ internal “program counter” and assembly offset. The assembly offset is where 64tass is placing bytes in the ROM. The program counter represents where 64tass treats the location as. For example, you might want something to be placed at
$000000 in the ROM (the assembly offset) but have pointers to it treat it as being at
$808000 (the program counter). Having both of these makes relocatable/memory mapped code much easier to make.
You can read from it to get the current program counter value and can write to it to set both the program counter and assembly offset. There are assembler directives to mess with the program counter value without setting the assembly offset.
Things defined between
.weak and a
.endweak directives will be overridden by duplicate symbols that are outside of the
.endweak but are within the same scope.
This can come in handy for default values.
64tass doesn’t have any way to check whether some symbol is defined, and there are no plans to add something similar to the
if(n)def directives you might see in other assemblers/compilers.
It does, however, support assigning a default value to a variable if the variable doesn’t exist. We can use this to mimic guards found in other assemblers/compilers to avoid assembling something twice:
#ifndef GUARD_THING #define GUARD_THING // Put the things that should only be assembled once here. #endif // GUARD_THING
could be written in 64tass as
GUARD_THING :?= false .if (!GUARD_THING) GUARD_THING := true ; Put the things that should only be assembled once here. .endif ; GUARD_THING
The first time this is encountered,
GUARD_THING will be given the default value of
.if block will be entered and
GUARD_THING will be given the value of
true. Then, on any subsequent pass,
GUARD_THING will not be given a default value and its persistent
true value will keep the
.if block from being entered again.
Instead of explaining this myself, here’s a video:
For our purposes there are a few things we’ll want.
This is a custom function that takes some address as a parameter and returns the corresponding address in the (LoROM only currently) SNES memory map. For example,
mapped($000000) == $808000.
This is mostly convenience (because figuring out where something goes in the memory map is annoying) but it also helps prevent human errors.
As a note, I’ve previously flip-flopped between doing this by hand, and I’ve also used other names for this function (such as
mapped() implementation for the curious
mapped .function Offset Return := ((Offset >> 15) << 16) | (Offset & $7FFF) | $8000 .if ((Offset >= $7E0000) || (Offset < $400000)) Return |= $800000 .endif .endf address(Return)
As mentioned before, 64tass has directives for messing with the program counter without affecting the assemble offset.
.logical is one of them. It takes a value as a parameter and sets the program counter to that value. For example:
* := $000000 .logical $808000 SomeLabel .long Somelabel .here
00 80 80, which is a little-endian long pointer (
$808000) to the first byte in the FastROM mirror of the SNES LoROM memory mapping (boy, that’s a mouthful).
Instead of typing out the parameter (
$808000 in this case), we can use the
mapped() function described above:
* := $000000 .logical mapped($000000) ; Things here .here
I’d love it if I could combine both the
* := $000000 and
.logical mapped($000000) lines into a single macro, but 64tass doesn’t appreciate
.logical in macros without the ending
If you’re familiar with Event Assembler buildfiles, you’ve probably seen a construction like
PUSH ORG SomeOffset POIN SomeThing POP SomeThing: // Stuff here
in an installer that gets assembled to freespace. The
POP bit goes to a fixed location to put a hook/reference/whatever to something else that can go anywhere.
I’ve implemented something like EA’s
POP in 64tass before using a combination of stacks and
.here, but it felt pretty awful to use:
PUSH/POP implementation for the curious
.cpu "65816" ; Helper function, the opposite of mapped() unmapped .function Address .endf ((((Address >> 16) & $7F) << 15) | (Address & $7FFF)) ; PUSH/POP implementation PushPopStack :=  push .segment Value=* PushPopStack ..= [\Value] .endm pop .segment - := PushPopStack[-1] * := unmapped(-) PushPopStack := PushPopStack[:-1] .endm ; Example * := $000000 .logical $808000 .word 0, 1, 2 SomeLabel .push .here * := $000020 .logical $808020 .long SomeLabel .here .pop .logical - .word 3, 4, 5 .here
Managing all of the
.here parts becomes annoying fast.
Instead of jumping out of/back into freespace, I keep the fixed and relocatable segments separate. 64tass has an easy way to do this while keeping everything in the same file for easy organization. This is done through the
.section takes a name as a parameter and collects all of the assembly between it and the next
.send. None of the contents are actually assembled into bytes and placed into the output until a
.dsection directive is used with the same name as a parameter.
You can have multiple
.sections with the same name and they’ll all be collected into the same group.
Here’s an example of how they could be used:
* := $000000 .logical mapped($000000) SomeFixedLocationHook jsl SomeRelocatableThing rts .here .section RelocatableSection SomeRelocatableThing php rep #$30 lda #$0001 plp rtl .send RelocatableSection
and in file
.cpu "65816" .include "Installer.asm" ; Some freespace * := $001000 .logical mapped($001000) .dsection RelocatableSection .here
These directives and the structure of the files above are what this thread is really about.
I’d like to be able to package code, tables, graphics, etc. for distribution and convenient editing, but I’ve always found it hard to organize things. These days I’m leaning toward packaging like content together into a single installer. For example, an installer for map sprites would contain the various tables you’d need to edit, all of the assembly you’d need to edit to relocate those tables, and the graphics for the map sprites themselves.
A build script (or buildfile or whatever you’d like to call it) collects all of the installers and manages other inclusions and freespace management.
The installer format that I’ve come up with has three major segments: definitions, fixed location components, and freespace components. Let’s take a look at an example and break down all of the parts.
;  .weak WARNINGS :?= "None" .endweak ;  GUARD_EXAMPLE_INSTALLER :?= false .if (GUARD_EXAMPLE_INSTALLER && (WARNINGS == "Strict")) .warn "ASM file included more than once." ;  .elsif (!GUARD_EXAMPLE_INSTALLER) GUARD_EXAMPLE_INSTALLER := true ;  .include "SomeDefinitionFile.h" ;  ; Definitions .weak rlSomeRoutine :?= address($808080) SomeConstant :?= 1 .endweak structSomeStruct .struct Foo, Bar Foo .byte \Foo Bar .byte \Bar .ends ;  ; Fixed location inclusions * := $001000 .logical mapped($001000) rlFooRoutine ;  .al .xl .autsiz .databank ? ; Sets the Bar in each entry of aSomeTable. ; Inputs: ; None ; Outputs: ; None pha phx ldx #0 _Loop lda aSomeTable+structSomeStruct.Bar,x cmp #SomeConstant beq _Found inc x inc x cpx #size(aSomeTable) bne _Loop clc bra _End _Found sec _End plx pla rtl .databank 0 .here ;  ; Freespace inclusions .section SomeStandaloneFunctionSection rlStandaloneFunction .autsiz .databank ? ; Twiddles some frobs ; Inputs: ; None ; Outputs: ; None php rep #$20 lda #SomeConstant jsl rlSomeRoutine plp rtl .databank 0 .send SomeStandaloneFunctionSection .section SomeTableSection ;  aSomeTable .include "TABLES/SomeTable.csv.asm" .send SomeTableSection ;  .endif ; GUARD_EXAMPLE_INSTALLER
I’ve been playing around with the idea of using a warning level variable independent of 64tass’ warnings. Setting the value of
WARNINGS on the commandline could enable different diagnostics, such as the
file included more than once message in the example. I set the warning level on the commandline with something like
-D WARNINGS=\"Strict\" (The extra backslashes are so that the quotes don’t get eaten).
We wrap things up in our guards to prevent things from being included/assembled multiple times. This is less important for ASM, because you’re unlikely to accidentally include it twice, but is very important for definition files that might be shared across multiple installers.
I think that just before the definitions block is the best place to put definition and library inclusions.
I like to wrap some values in a
.weak block so that they may be overridden, such as a routine that is at some known offset in vanilla but might be relocated or replaced.
Things here shouldn’t be assembled into any bytes.
This block is for things that are required to be at a certain place, like routine replacements, hooks, or anything else that isn’t put in freespace.
The first part of the header is a set of assembler directives that ensure that the routine assembles correctly, setting proper register sizes. It also informs the reader of how they need to enter the routine.
The second part is a set of comments telling what the routine does and what inputs/outputs it might have.
This block is where sections go. I like to split most things into individual sections to give finer control over where they go and how much space they take. When distributing an installer, the end user will be responsible for
.dsectioning these into free space. If the user forgets to put them somewhere, the assembler will throw an error. Thus, the user must consciously place them into freespace, which should hopefully help prevent accidentally assembling things to bad locations.
As a small note, I prefer tables to be generated from .csv files automatically as part of a make rule, but there’s nothing stopping you from typing out a table in your installer.
I find it nice to include what the .if block was for in a comment.
A build script is how you organize your project’s components, so it’s important for things to be neat and clear.
Here’s an example build script:
;  .cpu "65816" ;  .weak WARNINGS :?= "None" .endweak ;  ; Definitions .include "SomeLibrary.h" ;  ; Fixed location inclusions .include "SomeFile.asm" .include "AnotherFile.asm" ;  ; Freespace inclusions * := $012345 .logical mapped($012345) .dsection ThingSection .here * := $080000 .logical mapped($080000) .dsection AnotherThingSection .dsection FooSection .here
This line can be replaced by a commandline option, if you like that better.
This isn’t a bad spot to put this, too.
Libraries should be put here. Most installers should include their own definitions. Avoid placing individual definitions here and opt to put them in their own files.
These are your installers that contain at least one fixed location segment.
* := and
.logical lines should be kept within these files, if possible.
This segment contains any number of
.logical segments that define areas of freespace. If an installer doesn’t contain any fixed location components it can be included directly within a freespace segment. Installers with fixed location components will instead include the file in the fixed location part of the build script and should use
.dsection to place sections into freespace.