(FE5/SNES) Buildfile Guide

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 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

(repo)

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 .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:

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 .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.

[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.

17 Likes