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 buildfile
s a lot. The word buildfile
covers both a single file, sometimes called the build script
, and a project organizational structure called the buildfile method
.
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.
The Guide
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!
Old Thread
Purpose
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.
Prerequisites
Assembler stuff
The format for all of this will be for the assembler 64tass.
There are a few 64tass features that are especially important:
*
(Yes that’s just an asterisk)
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.
.weak
and .endweak
Things defined between .weak
and a .endweak
directives will be overridden by duplicate symbols that are outside of the .weak
/.endweak
but are within the same scope.
This can come in handy for default values.
:?=
(Default assignment)
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 false
. The .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.
SNES Memory Map
Instead of explaining this myself, here’s a video:
Memory Mapping - Super Nintendo Entertainment System Features Pt. 09 - YouTube
For our purposes there are a few things we’ll want.
mapped()
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 lorom()
).
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)
.logical
and .here
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
assembles to 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 .here
.
.section
, .send
, and .dsection
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 PUSH
/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 PUSH
/POP
in 64tass before using a combination of stacks and .logical
/.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 .logical
and .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
, .send
, and .dsection
directives.
.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 .section
s with the same name and they’ll all be collected into the same group.
Here’s an example of how they could be used:
In file Installer.asm
* := $000000
.logical mapped($000000)
SomeFixedLocationHook
jsl SomeRelocatableThing
rts
.here
.section RelocatableSection
SomeRelocatableThing
php
rep #$30
lda #$0001
plp
rtl
.send RelocatableSection
and in file Build.asm
.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.
Installers and Build Scripts
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.
Installers
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.
; [1]
.weak
WARNINGS :?= "None"
.endweak
; [2]
GUARD_EXAMPLE_INSTALLER :?= false
.if (GUARD_EXAMPLE_INSTALLER && (WARNINGS == "Strict"))
.warn "ASM file included more than once."
; [3]
.elsif (!GUARD_EXAMPLE_INSTALLER)
GUARD_EXAMPLE_INSTALLER := true
; [4]
.include "SomeDefinitionFile.h"
; [5]
; Definitions
.weak
rlSomeRoutine :?= address($808080)
SomeConstant :?= 1
.endweak
structSomeStruct .struct Foo, Bar
Foo .byte \Foo
Bar .byte \Bar
.ends
; [6]
; Fixed location inclusions
* := $001000
.logical mapped($001000)
rlFooRoutine
; [7]
.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
; [8]
; 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
; [9]
aSomeTable .include "TABLES/SomeTable.csv.asm"
.send SomeTableSection
; [10]
.endif ; GUARD_EXAMPLE_INSTALLER
[1] - Warnings
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).
[2], [3], [10] - if(n)def
replacements
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.
[4], [5] - Definitions
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.
[6] - Fixed location inclusions
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.
[7] - Routine header
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.
[8] - Freespace inclusions
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 .dsection
ing 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.
[9] - Tables
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.
[10] - .endif
I find it nice to include what the .if block was for in a comment.
Build Scripts
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:
; [1]
.cpu "65816"
; [2]
.weak
WARNINGS :?= "None"
.endweak
; [3]
; Definitions
.include "SomeLibrary.h"
; [4]
; Fixed location inclusions
.include "SomeFile.asm"
.include "AnotherFile.asm"
; [5]
; Freespace inclusions
* := $012345
.logical mapped($012345)
.dsection ThingSection
.here
* := $080000
.logical mapped($080000)
.dsection AnotherThingSection
.dsection FooSection
.here
[1] - CPU target
This line can be replaced by a commandline option, if you like that better.
[2] - Warnings again
This isn’t a bad spot to put this, too.
[3] - Definitions
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.
[4] - Fixed location inclusions
These are your installers that contain at least one fixed location segment. * :=
and .logical
lines should be kept within these files, if possible.
[5] - Freespace inclusions
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.