S4 Projection Layer — USD Scene Authorship: Lessons Learned

Audience: Engineers new to the GaiaFTCL S4 / C4 spatial projection pipeline.
Updated: 2026-05-05

---

What the S4 layer is

The M⁸ manifold has eight dimensions: four structural (S1–S4) and four constitutional (C1–C4).

S4 is the observable dimension — the thing you can *see* in the 3-D window.

Every USD scene is a projection surface for the S4 dimension. RealityKit renders it; the C4 system overlays live constitutional state on top of it.

A scene is not decorative. It is the spatial substrate that findEntity(named: entry.scope) anchors the CatalogAnchor entity onto. If the USD hierarchy is wrong the app enters TERMINAL STATE: REFUSED and shows nothing.

---

The catalog contract

FranklinSceneDirector.catalog is the single source of truth. Each entry has:

field purpose
sceneID key used by activeSceneID, domain portals, and the NATS routing layer
file base name with .usda extension — stripped by dropLast(5) in loadStrictS4Projection
scope must exactly match the second-level def Xform name inside the USDA file
franklinCue the single line of text rendered at the bottom of FranklinSceneView — write it for a researcher, not a developer

Lesson 1 — scope mismatch is silent until runtime.

The first LithographyDomain.usda was authored with def Xform "LITHO" but the catalog had scope: "LITHOGRAPHY". findEntity(named:) returns nil, the scene throws topologyAnchorNotFound, and the app shows REFUSED. The catalog is the contract; the USD must spell the name the same way.

---

Required USD hierarchy for every scene

def Xform "<FileName>" (kind="group")        ← defaultPrim
    def Xform "<SCOPE_NAME>" (kind="group")  ← scope — must match catalog entry
        def Xform "CatalogAnchor"            ← this is where manifold sphere + portals attach
            …geometry children…

CatalogAnchor is found by name in materializeS4Projection and passed to ensureOverlayEntities. If it is absent, the manifold sphere and domain portals are mounted on the scope root instead — they still appear, but their spatial origin is wrong.

Lesson 2 — CatalogAnchor is the M⁸ origin, not just a placeholder.

The sphere that shows live C4 state, the cone of safety, and the domain portal planes are all parented to CatalogAnchor. Place it at the physical centroid of the domain's primary geometry.

---

macOS vs visionOS loading

#if os(visionOS)
    rootScene = try await Entity(named: fileName, in: realityKitContentBundle)
#else
    guard let url = Bundle.module.url(forResource: fileName, withExtension: "usda") else {
        throw S4ProjectionError.sceneLoadFailed(fileName, "not found in module bundle")
    }
    rootScene = try await Entity(contentsOf: url)
#endif

Lesson 3 — Bundle.module is the SPM package bundle, not Bundle.main.

Resources declared as .process("Resources") in the target's Package.swift are copied into a <TargetName>_<TargetName>.bundle sub-bundle at build time. Always use Bundle.module to locate them. Bundle.main will miss them in both debug and release builds.

Lesson 4 — #if canImport(RealityKitContent) does not work as a platform guard.

On macOS, RealityKitContent is a local SPM package that *does* import. The canImport check therefore passes on macOS and the code attempts Entity(named:in:realityKitContentBundle) — which fails because there is no compiled .rkassets bundle on macOS. Use #if os(visionOS) instead.

---

USD geometry: write for researchers, not for fill

Every quantum scene must make the algorithm *legible* from the geometry alone. A researcher opening the scene in Reality Composer Pro or usdview should understand which algorithm family they are looking at before reading any label.

Circuit family (QC-CIRCUIT-001 · 5 algorithms)

Variational family (QC-VARIATIONAL-001 · 4 algorithms)

Linear algebra family (QC-LINALG-001 · 3 algorithms)

Simulation family (QC-SIMULATION-001 · 2 algorithms)

Bosonic family (QC-BOSONIC-001 · 2 algorithms)

Error-correction family (QC-ERRORCORR-001 · 3 algorithms)

Quantum proof scene (QUANTUM-PROOF-001 · projection probe)

---

The 19-algorithm count

The OQ test OQ-QM-007 asserts SUM(algorithm_count) = 19 across all language_game_contracts rows where domain LIKE 'quantum%'.

family algorithms count
QC-CIRCUIT-001 Shor, Grover, QFT, QPE, AmplitudeAmplification 5
QC-VARIATIONAL-001 VQE, QAOA, VariationalClassifier, QuantumAnnealing 4
QC-LINALG-001 HHL, QSVT, qPCA 3
QC-SIMULATION-001 QuantumWalk, HamiltonianSimulation 2
QC-BOSONIC-001 BosonSampling, GaussianBosonSampling 2
QC-ERRORCORR-001 SteaneCode, SurfaceCode, TopologicalQEC 3
Total 19

The algorithm_count column is set in SubstrateSchema.swift migration v11_language_game_algorithm_count. The USD scenes and catalog entries are the spatial face of this data — the geometry count must agree with the database count.

Lesson 7 — USD scenes, catalog entries, and language_game_contracts rows form a three-way contract.

If you add an algorithm: (1) update the algorithm_count in the migration, (2) add or update the corresponding USD scene, (3) update the franklinCue in the catalog. All three must change together or the S4 projection diverges from the substrate truth.

---

Adding a new scene

1. Write the USDA file following the hierarchy above (<FileName>/<SCOPE>/<CatalogAnchor>/…).

2. Place it in Sources/GaiaFTCL/Resources/ (macOS SPM bundle, loaded by Bundle.module).

3. Copy it to Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/ (visionOS Reality Composer Pro bundle).

4. Add a SceneEntry to FranklinSceneDirector.catalog with the correct scope name.

5. If it is a quantum domain: add a language_game_contracts row in a new SubstrateSchema migration with algorithm_count set correctly.

6. Run swift build --target GaiaFTCLScene — zero errors = catalog is syntactically correct.

7. Run swift run GaiaFTCLApp — select the new scene from the domain portal — confirm no REFUSED state.

---

Common failure modes

symptom root cause fix
REFUSED: topologyAnchorNotFound scope in catalog ≠ def Xform name in USDA Align the two strings
REFUSED: sceneLoadFailed … not found in module bundle File not in Sources/GaiaFTCL/Resources/ or wrong extension Check filename + Resources directory
Manifold sphere appears at origin (0,0,0) CatalogAnchor entity absent from scene Add def Xform "CatalogAnchor" as child of scope
Scene loads on visionOS, fails on macOS #if canImport(RealityKitContent) guard used instead of #if os(visionOS) Replace with os(visionOS)
OQ-QM-007 FAIL: algorithm_count sum ≠ 19 New algorithm added without updating migration Add UPDATE language_game_contracts SET algorithm_count = N WHERE game_id = '…'

---

USD REFUSED state declarations (2026-05-05)

When a USD prim references external assets or capabilities that are not yet

authored, the correct approach is to declare an honest REFUSED terminal state

in the USD — not to omit the prim.

Lesson 8 — An absent prim is worse than a REFUSED prim.

An absent prim silently changes the scene graph topology. REFUSED is a first-class

terminal state. A REFUSED prim with a vqbit:closureCondition tells every future

engineer exactly what is needed to close it to CALORIE.

Avatar prim pattern (Franklin.usda)

def Xform "Avatar" (
    doc = "Franklin avatar slot. Currently REFUSED — placeholder geometry."
    customData = {
        string "vqbit:primID"           = "FRANKLIN-AVATAR-PLACEHOLDER-001"
        string "vqbit:rigState"         = "REFUSED"
        string "vqbit:rigStateReason"   = "asset_not_authored"
        string "vqbit:closureCondition" = "Replace Cube with rigged FranklinFigure.usdz; flip rigState to CALORIE"
        bool   "vqbit:requiresClosureReceipt" = 1
    }
) {
    custom token vqbit:rigState = "REFUSED" (
        allowedTokens = ["CALORIE", "CURE", "REFUSED", "BLOCKED"]
    )
    def Cube "Placeholder" { double size = 0.4 }
}

Attachment state pattern (VQbitUIProtocol.usda)

For portal prims whose RealityKit .attachments binding is not yet implemented:

customData = {
    string "vqbit:attachmentState"       = "REFUSED"
    string "vqbit:attachmentStateReason" = "single_realityview_owner_refactor_pending"
    string "vqbit:closureCondition"      = "Promote to single RealityView with .attachments anchored to portal entity; flip to CALORIE"
}
custom token vqbit:attachmentState = "REFUSED" (
    allowedTokens = ["CALORIE", "CURE", "REFUSED", "BLOCKED"]
)

Lesson 9 — allowedTokens makes terminal states machine-readable.

Tools, test suites, and the qualification runner can grep for the set of allowed

values and assert exactly one is present. Without allowedTokens, the token

field is free text and cannot be validated programmatically.

Integrity guard

Scripts/check-no-duplicate-franklin-usda.sh fails the build if more than one

Franklin.usda exists in the repo. The authoritative copy is always

Sources/GaiaFTCL/Resources/Franklin.usda. The Packages/RealityKitContent/

copy was deleted in the 2026-05-05 session.

Lesson 10 — Two copies of a sovereign scene file is a data integrity violation.

Both copies diverge on the first edit. Delete the non-authoritative copy and

add a build guard that fails if it reappears.

---

S4 as world — the architectural correction (2026-05-10)

The first AlignmentImmersiveScene used Franklin.usda's Sovereign group as a

backdrop: a 0.24m-radius floating plinth with a rotating disc, AuraRing, and four

S4 arms. Instruments were spawned on top of the plinth. This was the wrong

abstraction at every level.

S4 is not a stage. S4 is the world the language game inhabits.

A child measuring soil temperature stands on a farm. A plasma physicist stands in

a control room. A doctor stands in an ED bay. The S4 projection space IS what the

operator can see. The whole visible world of the game. C4 does not live at the

centre — C4 enters from above as sky, ceiling, light.

The floating plinth abstraction smuggled the old "stage performance" metaphor into

the substrate: instrument on pedestal, abstract background, operator watching. This

is the wrong model. The operator IS in the world. The instrument is grounded in the

world's surface. The world's physical details encode S4 dimensions.

Lesson 11 — S4 is the world, not a stage.

Every USD scene serving as a language game backdrop must be a physical environment

the operator inhabits — not a platform they observe. The test: can the operator

describe what room they are in without being told? If not, the scene fails.

Lesson 12 — USD world authorship principles for S4 environments.

1. Ground at y=−0.475. All world scenes use soil/floor at y=−0.475. Instruments

land with bulb/base at y≈−0.46 (touching or slightly below the surface).

2. C4 is the sky. The SkyDome (Y-scale=−1 inverted sphere, radius 7–10m) at

world origin IS the C4 layer. Light enters from above. Do not put C4 geometry

on the floor or at the sides.

3. S4 dimensions encode in scene elements. Map every world element to an S4

or C4 dimension explicitly in the USD prim name and in this doc. S1 = structure

(soil, walls), S2 = temporal (clock, sun, mist), S3 = spatial (fence, room

boundary), S4 = observable (thermometer, display, diagnostic output).

4. No floorPlane() when a world is loaded. The world file provides the floor.

floorPlane() is a fallback for turns with no world file.

5. Supporting elements are not decoration. Farmhouse, tractor, fence — each

one places the operator in a context that justifies why this particular

measurement matters. The geometry is a constitutional argument.

6. Author at 1:1 scale. A 9×8m field is 9 metres wide. A thermometer tube

is 0.55m tall. Real-world scale in a 1.2m-from-camera scene forces the operator

to feel the measurement, not observe it abstractly.

Lesson 13 — FarmDawn.usda as the canonical example.

Sources/GaiaFTCL/Resources/FarmDawn.usda is the reference implementation of

a sovereign S4 world. Read it before authoring ControlRoom.usda or ED.usda.

Pay attention to:

Federation cosignature: pending operator signing host (v26). Witness (sha256 of rendered body): ea0df4d9f9a65c89e68e5dd6f2618c65f43dfefb050cbfdfc7700d91cb415678. This page serves with a substrate-honest pending-signature notice until the operator's Franklin signer cosigns it.