# Pyrite64 Build Process: How Scene Builders Translate Editor Data to N64 Binaries

> Learn the Pyrite64 build process. Discover how scene builders translate editor data to N64 binaries using JSON, SceneBuilder, and the N64 SDK toolchain to create ROMs.

- Repository: [Max Bebök/pyrite64](https://github.com/hailtododongo/pyrite64)
- Tags: internals
- Published: 2026-02-19

---

**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`](https://github.com/HailToDodongo/pyrite64/blob/main/src/project/project.cpp), [`src/build/projectBuilder.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/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`](https://github.com/HailToDodongo/pyrite64/blob/main/src/build/t3dmBuilder.cpp), [`src/build/fontBuilder.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/src/build/fontBuilder.cpp) |
| **3. Scene serialization** | The editor stores scenes as JSON ([`scene.json`](https://github.com/HailToDodongo/pyrite64/blob/main/scene.json)). `Project::Scene::serialize()` and `Project::Object::serialize()` turn this hierarchy into a JSON DOM. | [`src/project/scene/scene.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/src/project/scene/scene.cpp), [`src/project/scene/object.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/src/project/scene/object.cpp), [`src/utils/jsonBuilder.h`](https://github.com/HailToDodongo/pyrite64/blob/main/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`](https://github.com/HailToDodongo/pyrite64/blob/main/src/build/sceneBuilder.cpp), [`src/utils/binaryFile.h`](https://github.com/HailToDodongo/pyrite64/blob/main/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`](https://github.com/HailToDodongo/pyrite64/blob/main/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`](https://github.com/HailToDodongo/pyrite64/blob/main/src/build/projectBuilder.cpp), [`src/utils/proc.h`](https://github.com/HailToDodongo/pyrite64/blob/main/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`](https://github.com/HailToDodongo/pyrite64/blob/main/src/build/sceneBuilder.cpp), [`src/renderer/scene.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/src/renderer/scene.cpp) |

## Step 1: Project Loading and Asset Discovery

The build process begins in [`src/build/projectBuilder.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/src/build/projectBuilder.cpp). The `ProjectBuilder` class orchestrates the entire pipeline by first instantiating the project configuration:

```cpp
// 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`](https://github.com/HailToDodongo/pyrite64/blob/main/src/project/scene/scene.cpp), the `serialize()` method constructs the scene representation:

```cpp
// 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`](https://github.com/HailToDodongo/pyrite64/blob/main/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`](https://github.com/HailToDodongo/pyrite64/blob/main/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:

```cpp
// 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:

```cpp
// 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:

```cpp
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`](https://github.com/HailToDodongo/pyrite64/blob/main/src/build/projectBuilder.cpp):

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

```cpp
// 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:

```bash
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`](https://github.com/HailToDodongo/pyrite64/blob/main/src/renderer/scene.cpp) reads the compact binary format directly:

```cpp
// 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`](https://github.com/HailToDodongo/pyrite64/blob/main/src/project/scene/scene.cpp), where the editor saves object hierarchies and configuration.
- **SceneBuilder** in [`src/build/sceneBuilder.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/src/build/sceneBuilder.cpp) translates this JSON into compact binary files (`s####` and `s####o`), quantizing rotations into 32-bit quaternions and packing component data efficiently.
- **ProjectBuilder** indexes all assets via `addAsset()` and generates a custom Makefile from `data/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`](https://github.com/HailToDodongo/pyrite64/blob/main/scene.json) within each scene's data directory. The `Project::Scene::serialize()` method in [`src/project/scene/scene.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/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`](https://github.com/HailToDodongo/pyrite64/blob/main/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`](https://github.com/HailToDodongo/pyrite64/blob/main/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`](https://github.com/HailToDodongo/pyrite64/blob/main/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.