Pyrite64 Build Process: How Scene Builders Translate Editor Data to N64 Binaries
The Pyrite64 build process converts hierarchical JSON scene definitions into compact binary formats using SceneBuilder, then packages them into a Nintendo 64 ROM via a custom Makefile and the N64 SDK toolchain.
Pyrite64 is an open-source game engine designed specifically for Nintendo 64 development. Understanding the Pyrite64 build process is essential for developers who want to optimize how editor-created scenes become runtime-efficient N64 binaries. This article breaks down exactly how the engine's scene builders transform hierarchical JSON data into the binary structures the console executes.
The Pyrite64 Build Pipeline Overview
When you press Build in the Pyrite64 editor, the following high-level pipeline executes:
| Step | What happens | Key source files |
|---|---|---|
| 1. Project loading | Project::Project reads the project’s JSON config and scans the filesystem for assets, scripts and scenes. |
src/project/project.cpp, src/build/projectBuilder.cpp |
| 2. Asset compilation | 3-D models, textures, fonts, audio, and prefabs are built with asset builders (buildT3DMAssets, buildTextureAssets, etc.). Each asset becomes an mkasset binary. |
src/build/t3dmBuilder.cpp, src/build/fontBuilder.cpp |
| 3. Scene serialization | The editor stores scenes as JSON (scene.json). Project::Scene::serialize() and Project::Object::serialize() turn this hierarchy into a JSON DOM. |
src/project/scene/scene.cpp, src/project/scene/object.cpp, src/utils/jsonBuilder.h |
| 4. Scene binary generation | Build::buildScene() walks the deserialized scene graph and writes compact binary representations of objects and metadata using Utils::BinaryFile. |
src/build/sceneBuilder.cpp, src/utils/binaryFile.h |
| 5. Asset-scene index | SceneCtx::addAsset() registers each generated asset in a lookup table for runtime path resolution. |
src/build/projectBuilder.cpp |
| 6. Makefile generation | ProjectBuilder creates a project-specific Makefile and invokes the N64 SDK (mkasset, objcopy, etc.). |
src/build/projectBuilder.cpp, src/utils/proc.h |
| 7. Final binaries | The SDK produces the ROM image and filesystem that the engine loads on the N64. | src/build/sceneBuilder.cpp, src/renderer/scene.cpp |
Step 1: Project Loading and Asset Discovery
The build process begins in src/build/projectBuilder.cpp. The ProjectBuilder class orchestrates the entire pipeline by first instantiating the project configuration:
// src/project/project.cpp
Project::Project(const std::string &path) {
// Reads project.json, scans for assets
}
This step populates the asset registry, identifying all 3D models, textures, and scripts that subsequent stages will compile.
Step 2: Scene Serialization to JSON
Before binary generation, the editor persists scene data as JSON. In src/project/scene/scene.cpp, the serialize() method constructs the scene representation:
// src/project/scene/scene.cpp
nlohmann::json Project::Scene::serialize(bool minify) {
Utils::JSON::Builder builder{};
builder.set(name)
.set("fbWidth", fbWidth)
.setArray<LayerConf>("layers3D", layers3D, writeLayer);
builder.doc["conf"] = conf.serialize();
builder.doc["graph"] = root.serialize();
return builder.doc.dump(minify ? -1 : 2);
}
Each Project::Object serializes its components, property overrides, and children via serializeObj in src/project/scene/object.cpp, creating a complete hierarchical DOM that SceneBuilder will later consume.
Step 3: Binary Scene Generation with SceneBuilder
The core translation from editor data to N64 binaries occurs in src/build/sceneBuilder.cpp. The Build::buildScene() function deserializes the JSON scene graph and emits compact binary files.
Object Structure and Transform Encoding
For each object in the hierarchy, writeObject() emits a fixed header followed by transform data:
// src/build/sceneBuilder.cpp
ctx.fileObj.write<uint16_t>(objFlags); // ACTIVE, HAS_CHILDREN, etc.
ctx.fileObj.write<uint16_t>(obj.id);
ctx.fileObj.write<uint16_t>(obj.parent ? obj.parent->id : 0);
ctx.fileObj.write<uint16_t>(0); // padding
ctx.fileObj.write(srcObj->pos.resolve(obj.propOverrides));
ctx.fileObj.write(srcObj->scale.resolve(obj.propOverrides));
auto &rot = srcObj->rot.resolve(obj.propOverrides);
uint32_t quatQuant = T3D::Quantizer::quatTo32Bit({rot.x, rot.y, rot.z, rot.w});
ctx.fileObj.write(quatQuant);
This encoding stores position and scale as vectors, while rotation is compressed into a 32-bit quaternion using tiny3d::Quantizer to save precious N64 memory.
Component Serialization
Components are written with a header indicating their type and size:
// After component data is written
ctx.fileObj.align(4);
uint32_t size = (ctx.fileObj.getPos() - compPos) / 4;
ctx.fileObj.posPush(compPos);
ctx.fileObj.write<uint8_t>(comp.id); // component ID
ctx.fileObj.write<uint8_t>(size); // size in 32-bit words
ctx.fileObj.posPop();
Each component's funcBuild callback (defined in Project::Component::TABLE) generates the payload specific to that component type.
Scene Metadata and Layer Configuration
After processing all objects, SceneBuilder writes the scene header:
ctx.fileScene.write<uint16_t>(sc->conf.fbWidth);
ctx.fileScene.write<uint16_t>(sc->conf.fbHeight);
ctx.fileScene.write(sceneFlags);
ctx.fileScene.writeRGBA(sc->conf.clearColor.value);
ctx.fileScene.write(objCount);
ctx.fileScene.write<uint8_t>(sc->conf.renderPipeline.value);
ctx.fileScene.write<uint8_t>(sc->conf.frameLimit.value);
ctx.fileScene.write<uint8_t>(sc->conf.filter.value);
ctx.fileScene.write<uint8_t>(0); // padding
// layer table (3D, PTX, 2D)
ctx.fileScene.write<uint8_t>(sc->conf.layers3D.size());
ctx.fileScene.write<uint8_t>(sc->conf.layersPtx.size());
ctx.fileScene.write<uint8_t>(sc->conf.layers2D.size());
ctx.fileScene.write<uint8_t>(0); // padding
The builder produces two files: s####o (object data) and s#### (scene header), stored in filesystem/p64/.
Step 4: Asset Indexing and Makefile Generation
Before invoking the N64 toolchain, ProjectBuilder registers all generated assets in src/build/projectBuilder.cpp:
// src/build/projectBuilder.cpp
if(entry.romPath.size() > 5) {
auto outNameNoPrefix = entry.romPath.substr(5); // strip "rom:/"
assetFileMap += "if(path == \"" + outNameNoPrefix + "\")return " +
std::to_string(assetList.size()) + ";\n";
}
assetList.push_back({entry.romPath, stringOffset,
static_cast<uint32_t>(entry.type), flags});
This creates a lookup table that maps rom:/ paths to integer indices for runtime resolution.
Step 5: N64 Toolchain Integration and ROM Creation
The final stage generates a project-specific Makefile by transforming a template:
// src/build/projectBuilder.cpp
auto makefile = Utils::replaceAll(
Utils::FS::loadTextFile("data/build/baseMakefile.mk"),
{
{"{{N64_INST}}", project.conf.pathN64Inst},
{"{{PROJECT_NAME}}", project.conf.name},
{"{{ASSET_LIST}}", Utils::join(filesSorted, " ")},
// …
});
The build system then executes:
make -C "<project_path>" -j8
This invokes the N64 SDK tools—including mkasset and objcopy—to package the s#### and s####o binaries into the ROM's filesystem. The resulting filesystem/p64/ directory contains the final assets that the engine reads via P64::FileSystem at runtime.
Runtime Scene Loading on N64
At runtime, the engine loads the binary files produced by the build process. The runtime loader in src/renderer/scene.cpp reads the compact binary format directly:
// src/renderer/scene.cpp (conceptual)
BinaryFile file = FS::loadBinary("filesystem/p64/s001");
readHeader(file);
for each object record:
read flags, id, parent, transforms, component block …
instantiate Renderer::Object, create components, build meshes …
This binary compatibility between the editor output and runtime loader ensures efficient memory usage on the N64's limited hardware.
Summary
- The Pyrite64 build process starts with JSON serialization in
src/project/scene/scene.cpp, where the editor saves object hierarchies and configuration. - SceneBuilder in
src/build/sceneBuilder.cpptranslates this JSON into compact binary files (s####ands####o), quantizing rotations into 32-bit quaternions and packing component data efficiently. - ProjectBuilder indexes all assets via
addAsset()and generates a custom Makefile fromdata/build/baseMakefile.mk. - The N64 toolchain packages these binaries into the ROM filesystem using
mkasset, producing the final cartridge image. - At runtime, the engine reads these binary files directly via
P64::FileSystem, reconstructing the scene graph on the N64 hardware.
Frequently Asked Questions
What file format does Pyrite64 use to store scenes in the editor?
Pyrite64 stores scenes as JSON files named scene.json within each scene's data directory. The Project::Scene::serialize() method in src/project/scene/scene.cpp constructs this JSON using Utils::JSON::Builder, storing configuration in a "conf" node and the object hierarchy in a "graph" node.
How does Pyrite64 compress rotation data for the N64?
The engine compresses rotation data into 32-bit quantized quaternions. In src/build/sceneBuilder.cpp, the writeObject() function resolves rotation values and applies T3D::Quantizer::quatTo32Bit() to pack the quaternion components into a single uint32_t, significantly reducing memory overhead compared to storing four floating-point values.
What is the role of SceneBuilder in the Pyrite64 build process?
SceneBuilder—implemented in src/build/sceneBuilder.cpp—is the critical translation layer that converts JSON scene definitions into N64-ready binary formats. The Build::buildScene() function walks the deserialized object hierarchy, calling writeObject() to emit compact binary records containing transforms, component data, and parent-child relationships to files named s#### and s####o.
How does the N64 runtime locate assets built by Pyrite64?
The runtime uses a path-to-index lookup table generated during the build process. In src/build/projectBuilder.cpp, the addAsset() method strips the rom:/ prefix from paths and generates conditional return statements mapping specific paths to integer indices. This lookup table is compiled into the ROM, allowing the runtime to resolve rom:/filesystem/p64/s001 style paths to direct memory offsets.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →