• Home
  • About
    • Machine Code Construction Yard photo

      Machine Code Construction Yard

      I do assembly programming on old machines and boat building.

    • Learn More
    • Email
    • Tumblr
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

SNES Assembly Adventure 10: Modular Programming

21 Apr 2022

Reading time ~9 minutes

Welcome back, Adventurer! Last time, we learned how to build our code with CMake.

This time, we’ll restructure our sprite demo to

  • separate assets into their own subdirectory
  • autogenerate assets and headers with CMake

At the end of this tutorial, you’ll have a nice project template you can reuse in the future. As always, the code can be found on GitHub.

Without further ado, let’s get started!

Keep Assets Separate From Code

We already saw a bit of code generation with CMake in the last tutorial. At the moment, our source and asset files live in the same directory. As your project grows, this can become unwieldy. So the first thing we’ll do is move the assets into their own subdirectory. Create a new subdirectory assets and move both SpriteColors.pal and Sprites.vra into it. Create a new file called CMakeLists.txt in the new subdirectory assets:

# snes_cradle/assets/CMakeLists.txt

cmake_minimum_required( VERSION 3.21 FATAL_ERROR )

We’ll add commands to it in a moment. Your directory structure should look like this now:

snes_cradle/
    assets/
        CMakeLists.txt
        SpriteColors.pal
        Sprites.vra
    cmake/
        CMakeCA65816Compiler.cmake.in
        CMakeCA65816Information.cmake
        CMakeDetermineCA65816Compiler.cmake
        CMakeTestCA65816Compiler.cmake
    src/
        CMakeLists.txt
        JoypadSprite.s
    CMakeLists.txt
    MemoryMap.cfg

If we try to build this, it will fail with the following error messages:

$ cmake --build build
[1/2] Building CA65816 object CMakeFiles\JoypadSpriteDemo.dir\src\JoypadSprite.s.o
FAILED: CMakeFiles/JoypadSpriteDemo.dir/src/JoypadSprite.s.o
ca65.exe --cpu 65816
         -s
         -o CMakeFiles\JoypadSpriteDemo.dir\src\JoypadSprite.s.o
         ~\snes_cradle\src\JoypadSprite.s
~\snes_cradle\src\JoypadSprite.s:69: Error: Cannot open include file 'Sprites.vra': No such file or directory
~\snes_cradle\src\JoypadSprite.s:70: Error: Cannot open include file 'SpriteColors.pal': No such file or directory
ninja: build stopped: subcommand failed.

As you can see, ca65 can no longer find Sprites.vra and SpriteColors.pal after we moved them into assets (The output has been edited for clarity, so your output may differ a bit). This problem can be resolved in two ways. We could change the commands in JoypadSprite.s to include relative paths, namely, .incbin "../assets/Sprites.vra" and .incbin "../assets/SpriteColors.pal", respectively. The assembler will then find the asset files with no problem. But this isn’t ideal. As the number of source and asset files grows, we’ll probably want to move them into a more granular directory structure. So each time we move a file, we would have to change every single file that references it. That’s how bugs and build errors are born.

The second option is to tell the ca65 assembler where to find include files. The documentation tells us that the --bin-include-dir command line argument is used for that. So we need to execute

$ ca65 --cpu 65816 --bin-include-dir ../assets -s -o JoypadSprite.o JoypadSprite.s

For the source code to assemble correctly. This puts us a bit in a pickle. The ca65 assembler uses two different command line arguments for file inclusion: one for (binary) files included with .incbin (--bin-include-dir), and another for .include (--include-dir or shorter -I).

The question we have to answer here is whether we want to use the CMake command target_include_directories for binary (.incbin) or source files (.include). I’ve decided to opt for the latter, as a SNES game project will probably include a bit more source files than binary/asset files.

For binary files, we’ll instead use the CMake command target_compile_options to add --bin-include-dir to our build command. Let’s edit assets/CMakeLists.txt:

# snes_cradle/assets/CMakeLists.txt

cmake_minimum_required( VERSION 3.21 FATAL_ERROR )

target_compile_options( ${PROJECT_NAME}
    PRIVATE "SHELL:--bin-include-dir ${CMAKE_CURRENT_SOURCE_DIR}" )

Here we tell CMake to add --bin-include-dir followed by the directory the CMakeLists.txt file is located in (here, assets) to the compiler flags for the target. Next, we need to tell CMake what to do with those flags. The SHELL part is necessary to avoid option de-duplication. See ca65 search paths for details.

Next, we need to add our new assets subdirectory to the main CMakeLists.txt in the project’s root directory file:

# snes_cradle/CMakeLists.txt

cmake_minimum_required( VERSION 3.21 FATAL_ERROR )

set( MEMORY_MAP_FILE ${CMAKE_CURRENT_SOURCE_DIR}/MemoryMap.cfg )
list( APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake" )

project( JoypadSpriteDemo
         LANGUAGES CA65816 )

add_executable( ${PROJECT_NAME} "" )
add_subdirectory( assets )
add_subdirectory( src )
set_target_properties( ${PROJECT_NAME}
    PROPERTIES SUFFIX ".smc" )

# Check if the path to the memory map file is set
if( NOT DEFINED MEMORY_MAP_FILE )
  message( FATAL_ERROR "Path to memory map file not set!" )
endif( NOT DEFINED MEMORY_MAP_FILE )

All we did here was add add_subdirectory( assets ). Finally, we need to update the build command itself in cmake/CMakeCA65816Information.cmake:

# snes_cradle/cmake/CMakeCA65816Information.cmake

# How to build objects
set( CMAKE_CA65816_COMPILE_OBJECT
    "<CMAKE_CA65816_COMPILER> --cpu 65816 \
                              <FLAGS> \
                              -s \
                              -o <OBJECT> \
                              <SOURCE>"
)

# How to build executables
set( CMAKE_CA65816_LINK_EXECUTABLE
    "ld65 -C ${CMAKE_SOURCE_DIR}/MemoryMap.cfg \
          <OBJECTS> \
          -o <TARGET>"
)

set( CMAKE_CA65816_INFORMATION_LOADED 1 )

We only need to add a single line again, namely, the <FLAGS> argument. Now rerun CMake and the error message from earlier should no longer show. As always, test the ROM you just built in your emulator and verify everything still works.

Creating a Header and Source File For Assets

We’ll need to generate a header so that different source files can access the addresses where the assets are stored. We want to be able to access assets/addresses with the .include directive. In contrast, if we included the assets with .incbin in each source file (that accesses the assets) it could copy the whole asset data several times into the ROM! That’d be a waste of precious ROM memory. Instead, we’re going to create a header and a source file for the assets. Thankfully, cc65 makes it easy for us to create something akin to header guards.

First, we’re going to create a new source file called assets/Assets.s:

This file essentially moves the SPRITEDATA segment from lines 68 through 70 of src/JoypadSprite.s into its own file. It also adds a couple of .export directives to make the symbols available to other parts of the code (i.e., modules). In general, all symbols are local to the files/modules they’re in. If you want to make them available to other sources/modules, you need to export them.

Next, we create the assets/Assets.inc header file:

This file simply imports the symbols we just exported from assets/Assets.s into each file that includes assets/Asset.inc. We add a header guard to protect the file from multiple inclusions (and therefore causing symbol redefined errors).

Since there’s a new file that needs assembling, we also update assets/CMakeLists.txt:

# snes_cradle/assets/CMakeLists.txt

cmake_minimum_required( VERSION 3.21 FATAL_ERROR )

target_compile_options( ${PROJECT_NAME}
    PRIVATE "SHELL:-I ${CMAKE_CURRENT_SOURCE_DIR}" )

target_sources( ${PROJECT_NAME}
    PRIVATE Assets.s )

Note that we replaced some options in target_compile_options. This is about --bin-include-dir VS --include-dir we talked about above.

Finally, we update src/JoypadSprite.s. Just replace lines 68 through 70 with `.include “Assets.inc”. Don’t forget to rerun CMake and test your ROM in an emulator.

Your directory structure should now look like this:

snes_cradle/
    assets/
        Assets.inc
        Assets.s
        CMakeLists.txt
        SpriteColors.pal
        Sprites.vra
    cmake/
        CMakeCA65816Compiler.cmake.in
        CMakeCA65816Information.cmake
        CMakeDetermineCA65816Compiler.cmake
        CMakeTestCA65816Compiler.cmake
    src/
        CMakeLists.txt
        JoypadSprite.s
    CMakeLists.txt
    MemoryMap.cfg

I chose to put all assets into a single header for convenience. But you can be as granular as you want. For example, it would probably be useful to be able to access your main character’s and enemy graphics separately. Just create a separate header for each (whether the graphic data itself goes into a separate module, depends on your ROM’s memory layout).

Refactoring JoypadSprite.s

Finally, we’re going to split up our main source file into smaller chunks. Several parts of the main source make use of the constants defined at the beginning of src/JoypadSprite.s. So let’s split those off for easy access. Create a new subdirectory src/common with three new header files in it:

As with the assets, you can get as granular with your headers as you want. I personally tend to keep my code in many separate files. As your code grows, it is a lot easier to merge several files into one later on than to split up code (several modules/subroutines/etc.)

As always, we also need a new src/common/CMakeLists.txt:

# snes_cradle/src/common/CMakeLists.txt

cmake_minimum_required( VERSION 3.21 FATAL_ERROR )

target_compile_options( ${PROJECT_NAME}
    PRIVATE "SHELL:-I ${CMAKE_CURRENT_SOURCE_DIR}" )

Modify src/CMakeLists.txt to include add_subdirectory( common ). Remove lines 6 through 62 in src/JoypadSprite.s and include the newly created headers with .include.

Now we’re going to factor out some of the subroutines related to graphics. Namely, LoadVRAM, LoadCGRAM, and UpdateOAMRAM. Those functions are universal, you can use them in all kinds of projects. I choose to put them in a subdirectory called src/PPU, where I keep all code related to the PPU/graphics.

We’re going to follow a similar pattern as we did with the assets. Create a new header-source pair src/PPU/PPU.s and src/PPU/PPU.inc respectively:

Same idea as with the assets: We put the subroutines into their own file/module and then create a header so other files/modules know where to find the subroutines. While ca65’s smart mode is pretty good, it sometimes can go astray, especially when you’re code is spread among several files. So in lines 15 and 16, I tell the assembler explicitly which register sizes to assume. Mind that this does not generate any code. The assembler will still track rep and sep instructions but will need some help from time to time as your project’s code grows.

You should have a good understanding of it now, so I’m going to skip the necessary updates to the CMake files. If it gets confusing, you can see the final code on Github.

As a final step, we’ll move the VECTOR segment into its own file src/common/Vector.s, as well as create a separate src/Game/Init.s file:

The only thing left to do, is to refactor your src/JoypadSprite.s:

Now it only contains your main game loop, which I think is much nicer to handle and expand this way.

Your final directory structure should now look like this:

snes_cradle/
    assets/
        Assets.inc
        Assets.s
        CMakeLists.txt
        SpriteColors.pal
        Sprites.vra
    cmake/
        CMakeCA65816Compiler.cmake.in
        CMakeCA65816Information.cmake
        CMakeDetermineCA65816Compiler.cmake
        CMakeTestCA65816Compiler.cmake
    src/
        common/
            CMakeLists.txt
            GameConstants.inc
            MemoryMapWRAM.inc
            Registers.inc
            Vector.s
        Game/
            CMakeLists.txt
            Init.inc
            Init.s
        PPU/
            CMakeLists.txt
            PPU.inc
            PPU.s
        CMakeLists.txt
        JoypadSprite.s
    CMakeLists.txt
    MemoryMap.cfg

Mind that is directory structure is pretty arbitrary and you can and should tailor it to your own needs. If you got lost somewhere, check out the full code here.

Conclusion

This and the last entry may seem to overly complicate things. But I’m certain that this will pay off in the long run. As of now, we have a simple project structure that can easily be expanded without it causing us headaches. There are a few things I’ve skipped for the sake of brevity. For example, it is possible to completely automate the generation of header files from sources with CMake. But I opted to keep things simple for now.

Next time, we’ll finally return to more SNES programming. What’s a game without backgrounds? Exactly, boring.

Thank you for reading and see you next time!

Links and References

  • Another SNES project template by ARM9 that uses C
  • The SNESKIT by Daniel Oaks includes very useful extra tools and code templates
  • The full code of this tutorial


SNES Assembly AdventureassemblyprogrammingSNEStutorial Share Tweet
SNES Assembly Adventure 10: Modular Programming | Machine Code Construction Yard