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