Developer experiences from the trenches
Date | Revision | What’s new |
Aug 18th, 16 | 0.1 | initial; request for comments |
Aug 26th, 16 | 0.2 | Moved
lib to project-specific subdirs |
| | Address uncompiled shader scenarios |
Aug 29th, 16 | 0.3 | Add standardized platform names for binary dirs |
Sept 5th, 16 | 0.4 | Add support for
tools, justify public header files |
| | Address third party code not in vendors |
What follows is a structure for cross platform projects. By adhering to a standard, fewer mental cycles have to be expended navigating projects. Embedded in these project standards is experience gained 20+ years of navigating large game and tool projects. Hopefully they help a few people avoid mistakes.
What follows is a fairly exhaustive list of functionality. Most projects do not need most of this. The idea is that you use these standards if you need them, or you just exclude them if you don’t.
Some of this functionality makes sense for libraries and some of it makes sense for full applications.
What is checked in to the Code Project Root is everything that is checked in to a single source repo. The assumption here is that your source control package is not that good at binaries, and so, it is used to handle source code only.
If you are into checking in all compiled assets, you can easily do so in the specified directories. Having a separate binary root is really a superset of the binary check-in approach.
See “Binary Project Root” below for more.
All filenames should be 7-bit ascii without spaces and lowercase. Prefer underscores over CamelCase.
Lowercase filenames prevent your build from breaking when porting to Linux and Android where case sensitivity really matters. It also avoids case conflicts when checking out two files with the same name but different cases on Windows that were checked in on Linux. Various source control programs have solutions for this, which means your library becomes potentially incompatible with different source control mechanisms. Just stay away from uppercase.
CamelCase is ultimately a matter of preference, but I have seen a lot of CamelCase users insert underscores anyway once their trees reach a certain degree of complexity. This creates mental effort trying to remember which files conform to two standards and how they conform.
Getting this right up front is important because you break compiles changing it later.
For the extent of our purposes,
x64 all refer to the same thing. Where possible, name things
x86 all mean the same thing. Name things
xxxROOT is an environment variable, containing the root of the current project.
xxx is an uppercase prefix of the project. The environment variable can be used to avoid relative paths manipulations in build scripts and build files which improves their readability.
xxxROOT is used instead of a unified name for all projects because projects could become nested, possibly as vendors. Example:
xxxBIN is the path to a corresponding binary repo root. Example:
NFDBIN. More on binary repos later on.
LICENSE — Needed for all published repos.
README.md — Use caps in name to conform to Github standard if publishing to Github.
Unbuilt docs go here, along with any build system needed to build the docs.
Built docs go to
Root for all project source. If source is conditionally compiled based on target architecture, it can go into subdirectories. ex:
Prefixes to filenames are preferred to directories. ex:
video_init.c instead of
video/init.c. This enables directories to signify being conditionally included into compiles while still allowing your file explorer to functionally order files by sorting alphabetically.
This directory contains private header files used to build the codebase. Private header files are not needed by users to compile against a library, but are needed for the library to compile itself.
It is not necessary to put private header files in a separate directory; file browsers can simply sort by type.
For most non-trivial renderers, shaders must be compiled. Therefore, shader source is treated like code and is checked in to the source repo.
Compiled shaders are treated like binary content and should be checked in to the binary project root (below) under the
Where shaders vary by renderer backend, the following directories can serve as examples:
For more trivial renderers, store the canonical, up-to-date shaders in the source repo and copy them over to your distribution tree verbatim on change. This ensures your shaders align with the code that is driving them in the current source revision.
xxx is the prefix name for the project. For example:
nfdconfig.h. This is put in its own subdirectory because it is sometimes included in other, linking projects. Doing so would mean adding the directory it’s in to the search path. We want to avoid adding
/src to the search path, exposing all private header files.
xxxconfig.h is a header file that contains configuration settings for different, named build configurations (such as
RELEASE). Where possible, each config option defines a value as
#if can be used in place of
#ifdef in the code. This enables brevity, but also removes a compile-time ambiguity between false and not defined. Doing so generates a compile error instead of a runtime bug.
#pragma once #ifdef DEBUG # define VERBOSE_LOG 1 #endif #ifdef RELEASE # define VERBOSE_LOG 0 #endif
And then, later on in source:
#include "xxxconfig.h" #if VERBOSE_LOG log("a lot of extra stuff") #endif
This is preferred over
#ifdef DEBUG because it allows additional configuration types to be created without having to comb the entire codebase.
xxxconfig.h is not generated by a build step; it should contain everything needed to build based on a single compiler define. However, if you decide you must build it, do not generate different configs based on the CPU architecture. Doing so creates real difficulties for fat binary architectures which must include your
In some cases, it is advantageous to insert third party code right into your own build and project rather than produce a library. The excellent STB libraries are one such example.
If the third party files do not follow the directory or naming conventions of the Native Project Standards, or if they become distastefully numerous, placing them in
/src/3rdparty reduces clutter.
Do not add an additional include search path to your compile settings for this directory. Doing so erases the opportunity to signify you are including lib code. Do this instead:
This directory contains public header files which are used to link against the library. If the project is not a library, just intermingle the header files with the source.
It is rare to need a public header file include directory for executable projects, but possible. An application may export settings for compatibility with plugins or mod support.
If you are building an executable project, consider removing this directory until it is expressly needed so as not to confuse developers who would put private header files here instead of in
/src where they are intended to go.
The preferred approach, if at all possible, is to use premake to generate project files. The project files should then be checked in, in subdirectories. This means Premake is a package maintainer’s tool, not a necessary dependency for user compilation.
The premake script can be checked in at
/build/premake5.lua. Virtually no other files should be in the build root, so new users aren’t confused and tempted to try building from here, but from a subdirectory.
Project subdirectories are in the form:
buildtype ideally conforms to a premake action. The intended compiler is the preferred descriptor for the second half of the directory name. Premake has an
--os parameter, which when coupled with
--action gmake, generates a makefile for a specific OS.
However, using the value of the
--os parameter is ambiguous for naming the build directory: what toolchain does a Makefile on Windows build for? Better to name the directory
gmake_mingw. Use your best judgment when naming the
<compiler> so that users know where to go when they search for the correct means to build your project.
Autobuild contains scripts that are necessary to build the application and all its vendors from scratch in all supported formats. The goal is to remove the number of build steps in the autobuild configuration, moving the logic to a distributed script. This simplifies your autobuild system, which is often a remarkably fragile piece of software.
For Frogtoss Games projects, this means containing a recursive
ftgcompile.py script which detects the target architectures available to build and generates them.
/build/<buildtype_compiler>/bin/<arch> is where compiled exes and DLLs go.
<arch> is not used on fat binary operating systems. Compiled binaries are not checked in here.
The binary name is appended with
_d if it was compiled in debug mode. For example:
/build/<buildtype_compiler>/obj is not checked in and contains build intermediates including
/build/<buildtype_compiler>/lib/<arch> is where compiled static libs go. Compiled libs are not checked in.
/tools is a top level directory, sibling to
/build. It contains small scripts and binaries that assist your project’s development in some way.
Many projects end up with multiple tools directories, often by accident. By making
/tools a top level directory, it is more likely to be known by developers, stopping them from making competing directories. It is disorderly to have multiple tools directories in a project.
/tools/bin exists, it is intended to be added to the user’s
PATH so scripts can be executed globally. Scripts here should have their executable bit script and use shebang lines where possible.
If a tool is useful for more than just your project and reaches a non-trivial scope, consider creating a different project for it and moving it out of
Vendors contains all third party code which is checked in to subdirectories. Code is expressly checked in — there are no git submodules. Submodules are not automatic and actually get in the way of diffing upgraded vendor libraries and browsing your git log.
/vendors is intentionally not under
/src, which means developers can recursively grep
/src or create symbol caches (etags, Source Insight, etc.) without capturing all vendor symbols. Alternatively, they can run their recursive tools from the project root and capture all vendor source if they like.
If unzipping a vendor generates a directory with a version, the version number is removed before check-in. Example:
/vendors/Python. This removes the need for reworking multiple build scripts when upgrading vendor versions.
For Frogtoss projects,
/vendors/ftgcompile.py will exist, which goes through each directory and compiles each vendor for each available target architecture, calling a corresponding
ftgcompile.py in each vendor’s subdirectory.
/vendors/lib/<arch> contains the compiled vendor libs. This is a repository for all built libs, and is the singular link directory that your application needs to link against all libraries for a given architecture.
In a Universal binary platform,
<arch> is omitted.
This directory’s contents is not checked in.
/vendors/include contains the public include files for all built libs. It is the singular include directory your application needs, hopefully.
Realistically, in order to achieve this, the
ftgcompile.py script responsible for compiling each vendor would need to move the public include files to this directory. This fails on fat binary operating systems where include files differ based on the CPU architecture being compiled. Your vendor is being terrible to you. In this case, you will have to handle this within the standards of the vendor’s library.
This directory’s contents is not checked in.
Unit tests should not produce separate binaries! Conditionally compile all unit tests in, POST-style, so they run on application startup. This avoids the need to separately build and run unit tests when porting to a new platform, something that inevitably happens later than core exe porting or never at all when targeting deploy-to systems such as consoles or mobile.
Tests that go here include everything that has to actually run the compiled binary to work properly. Ideally, these should be one file-per-program. If they are larger projects, consider creating another project root for them rather than cluttering them up.
Built tests (if they need to compile at all) go in
The binary, distributable project root sits sibling to the code project root. It does not nest. This allows you to use a separate binary-friendly source control system for built versions of your application.
This is a good strategy for binary-heavy cross platform games, but often overkill for smaller applications.
The assumption is that you have build servers which compile executables and check them in here. Therefore, one tree exists which contains all executables and dependencies and compiled asset resources simultaneously. Generating a distributable installer from the head of this source repo is how you distribute the most recent version of your project.
xxxBIN (described above) references this directory. This enables you to easily launch your application relative to the root of the binary folder. It also lets you check out multiple, differing versions of the binaries folder and test them with a single EXE just by changing the
xxxBIN environment variable path.
Executables go in
<arch> is an optional arch, if the platform needs to differentiate cpu architectures. Example:
Examples of standardized platform names:
If content isn’t packaged, it goes in
/content. It is largely up to the application how this is set up.
However, if there are base assets and game-specific assets,
/content/base should contain the base assets which are essential to the startup of the application. Examples of this would include a debug font and a mouse cursor. This allows for barebones distributables later on.