Adapter contract version: 1.0.0
Godot adapter version: 0.5.0
Status: Active — Phase 3 in-progress polish
This document is the authoritative reference for the Nova64 Godot host bridge. It covers:
call_bridge(method, payload) → Dictionary entry pointFor the general adapter surface that all backends must satisfy see
docs/ADAPTER_CONTRACT.md. For backend structure and ownership rules see
docs/BACKEND_RUNTIME.md.
JS (QuickJS) → engine.call(method, payload)
↓
nova64-godot/gdextension/src/bridge.cpp
Nova64Host::call_bridge(method, payload)
↓
GDScript node tree + Godot rendering APIs
Nova64 JS runs inside QuickJS embedded in a Godot GDExtension node
(Nova64Host). All rendering, input, audio, and asset work crosses the
bridge via engine.call(method, payload). No direct Godot API calls
exist in cart code or the JS shim.
The shim at nova64-godot/godot_project/shim/nova64-compat.js translates
cart-facing Nova64 API calls (createCube, setCameraPosition, etc.) into
call_bridge method+payload pairs. Cart code never calls engine.call
directly.
Nova64Host::call_bridge(method: String, payload: Dictionary) → Dictionary
method — dot-separated namespace and command, e.g. "material.create".payload — zero or more named parameters.{ "error": ..., "message": ... } on failure.Unknown methods return { "error": "unsupported_method" }.
Batched dispatch is available via engine.flush(commands) where commands
is an Array of [method, payload] pairs or {m, p} objects. Returns an
Array of result Dictionaries in the same order.
host.getCapabilities (or engine.init) returns a Dictionary:
{
"backend": "godot",
"contractVersion": "1.0.0",
"adapterVersion": "0.5.0",
"features": [ ... ]
}
The features array lists every method string that call_bridge will
handle without returning unsupported_method. Cart and runtime code should
use this array for feature detection rather than hard-coding backend checks.
*.create command returns { "handle": <integer> }.HandleTable.MATERIAL, GEOMETRY, MESH_INSTANCE, CAMERA, LIGHT,
TEXTURE, AUDIO, MULTI_MESH, PARTICLES.{ "error": "invalid_*_handle" }.transform.set resolves handles of kinds MESH_INSTANCE, CAMERA,
LIGHT, MULTI_MESH, and PARTICLES — all node-backed resource types.| Method | Payload | Returns |
|---|---|---|
host.getCapabilities |
— | { backend, contractVersion, adapterVersion, features[] } |
engine.init |
— | { capabilities: <same as above> } |
engine.flush |
commands: Array |
Array of per-command results |
| Method | Key payload fields | Returns |
|---|---|---|
material.create |
albedo (r/g/b/a), metallic, roughness, specular, emission, emissionEnergy, transparency ('alpha'/'scissor'/'hash'/'depth_prepass'), alphaCut, blend ('add'/'sub'/'mul'), unshaded, shadingMode ('unshaded'/'per_vertex'/'per_pixel'), diffuseMode ('lambert_wrap'/'toon'/'burley'), specularMode ('toon'/'disabled'), rim, rimTint, clearcoat, clearcoatRoughness, anisotropy, doubleSided, castShadow, receiveShadow, depthTest, depthWrite, textures (object of slot→handle maps) |
{ handle } |
material.destroy |
handle |
{ ok } |
material.emission |
handle, color (r/g/b), energy |
{ ok } |
material.blend.add |
handle |
{ ok } |
Colors are passed as { r, g, b, a } floats (0–1) or as a single number
encoding 0xRRGGBB.
All geometry commands return { handle }. Geometry handles are immutable
after creation; rebuild to change shape.
| Method | Key payload fields |
|---|---|
geometry.createBox |
w, h, d (default 1) |
geometry.createSphere |
radius (default 0.5), rings, segments |
geometry.createPlane |
w, h (default 1), subdivide |
geometry.createCylinder |
radius (default 0.5), height (default 1), segments |
geometry.createCone |
radius (default 0.5), height (default 1), segments |
geometry.createTorus |
innerRadius, outerRadius, rings, segments |
| Method | Key payload fields | Returns |
|---|---|---|
mesh.create |
geometry (handle), material (handle, optional) |
{ handle } |
mesh.setMaterial |
mesh (handle), material (handle) |
{ ok } |
mesh.destroy |
handle |
{ ok } |
mesh.createInstanced |
geometry (handle), count, material (handle) |
{ handle } |
transform.set applies to any node-backed handle (mesh, camera, light,
instanced mesh, particles).
| Method | Key payload fields | Returns |
|---|---|---|
transform.set |
handle, position ({x,y,z}), rotation ({x,y,z} radians), lookAt ({x,y,z}), scale ({x,y,z}), visible (bool) |
{ ok } |
If both lookAt and rotation are present, lookAt wins.
| Method | Key payload fields | Returns |
|---|---|---|
camera.create |
fov (default 60), near (default 0.1), far (default 1000) |
{ handle } |
camera.setActive |
handle |
{ ok } |
camera.setParams |
handle, fov, near, far |
{ ok } |
| Method | Key payload fields | Returns |
|---|---|---|
light.createDirectional |
color ({r,g,b}), energy (default 1.0), shadow (bool, default true) |
{ handle } |
light.createPoint |
color ({r,g,b}), energy (default 1.0), range (default 20), shadow (bool) |
{ handle } |
light.createSpot |
color, energy, range, angle (degrees), shadow |
{ handle } |
light.setColor |
handle, color ({r,g,b}) |
{ ok } |
light.setEnergy |
handle, energy |
{ ok } |
light.setSun |
energy, color, pitch (degrees), yaw (degrees) |
{ ok } |
light.setSun creates or updates a shared DirectionalLight3D used for
day/night cycles. It is separate from individually tracked light handles.
| Method | Key payload fields | Returns |
|---|---|---|
env.set |
fog (bool), fogColor ({r,g,b}), fogDensity, fogNear, fogFar, ambientColor ({r,g,b}), ambientEnergy, sky ('procedural'/'none'), skyColor ({r,g,b}), skyHorizonColor, glow (bool), glowIntensity, ssao (bool), tonemapper ('linear'/'reinhardt'/'filmic'/'aces') |
{ ok } |
input.poll takes no payload and returns a snapshot of current Godot input state.
{
"keys": ["KeyA", "Space"], // web-style key codes currently held
"left": true, // Arrow/WASD convenience bools
"right": false,
"up": false,
"down": false,
"action": false,
"buttons": [false, false, ...], // 14-element gamepad button array
"axis": { "lx": 0.0, "ly": 0.0, "rx": 0.0, "ry": 0.0, "lt": 0.0, "rt": 0.0 },
"touches": [{ "id": 0, "x": 100, "y": 280 }] // active touches, 640x360 logical
}
Touches echo whatever the GDScript host last pushed via set_touches
(Nova64Host can't enumerate active touches from the Input singleton, so the
host captures InputEventScreenTouch/Drag and pushes the live set each frame).
They surface to carts as nova64.input.touches() / touchCount().
Key codes use web KeyboardEvent.code names ("KeyW", "ArrowLeft",
"Space", "Enter", "ShiftLeft", etc.). The full mapped set is defined
in bridge.cpp at _cmd_input_poll.
Gamepad button array (buttons[0..13]) maps to the Nova64 gamepad layout:
0=ArrowLeft, 1=ArrowRight, 2=ArrowUp, 3=ArrowDown, 4=KeyZ, 5=KeyX, 6=KeyC, 7=KeyV, 8=KeyA, 9=KeyS, 10=KeyQ, 11=KeyW, 12=Enter, 13=Space.
Axes apply a shaped deadzone (0.18) and power curve (exponent 1.35).
| Method | Key payload fields | Returns |
|---|---|---|
texture.createFromImage |
data (Array of RGBA bytes), width, height, filter ('nearest'/'linear'), mipmap (bool, default true), srgb (bool) |
{ handle } |
texture.destroy |
handle |
{ ok } |
| Method | Key payload fields | Returns |
|---|---|---|
audio.loadStream |
path (Godot resource path), loop (bool) |
{ handle } |
audio.play |
handle, volume (0–1, default 1), pitch (default 1) |
{ ok } |
audio.stop |
handle |
{ ok } |
| Method | Key payload fields | Returns |
|---|---|---|
mesh.createInstanced |
geometry (handle), count (max instances), material (handle) |
{ handle } |
instance.setTransform |
handle, index, position, rotation, scale, color ({r,g,b,a}) |
{ ok } |
| Method | Key payload fields | Returns |
|---|---|---|
particles.create |
count (default 500), lifetime (seconds, default 1), color ({r,g,b,a}), speed (default 2), spread (degrees, default 45), gravity ({x,y,z}), directionX/Y/Z, emitting (bool, default true), position ({x,y,z}), geometry (handle, optional) |
{ handle } |
particles.destroy |
handle |
{ ok } |
| Method | Key payload fields | Returns |
|---|---|---|
model.load |
path (Godot resource path), position ({x,y,z}), rotation ({x,y,z}), scale ({x,y,z}) |
{ handle, childHandles[] } |
vox.load |
path (Godot resource path), scale (default 0.1), position ({x,y,z}) |
{ handle } |
This section defines the host-side commands nova64.video will use on
Godot once the Godot adapter ships. The JS API is already stable in
runtime/api-video.js; it currently falls through to a stub on Godot
hosts. When the bridge below is in place, videoApi(gpu)'s backend
detector will recognise Godot and route real calls through.
| Method | Key payload fields | Returns |
|---|---|---|
video.load |
path (Godot resource path, must resolve to a VideoStream-compatible file, typically .ogv or .webm), loop (bool), autoplay (bool), muted (bool) |
{ handle, ready } |
video.applyToMesh |
videoHandle, meshHandle, slot ('albedo' (default) / 'emission') |
{ ok } — host binds a VideoStreamPlayback's texture as the mesh material's albedo_texture / emission_texture |
video.play |
handle |
{ ok } |
video.pause |
handle |
{ ok } |
video.seek |
handle, time (seconds) |
{ ok } |
video.setVolume |
handle, volume (0–1) |
{ ok } |
video.playFullscreen |
path, loop (bool), muted (bool), skipKey (string, default 'ui_cancel') |
{ handle } — host spawns a fullscreen VideoStreamPlayer on the viewport; emits video.finished event when playback ends or skip-key is pressed |
video.destroy |
handle |
{ ok } |
Asset format: Godot's VideoStream core supports .ogv (Theora) out
of the box and .webm (VP8/9) via the webm plugin. .mp4 (H.264) is
not natively supported in Godot 4 and would require a third-party
plugin or transcoding to one of the above. The browser path uses .mp4
freely, so carts that want cross-host compatibility should ship both an
.mp4 and a transcoded .ogv / .webm and let the engine pick by
backend.
Events: the host MUST emit video.finished with payload
{ handle, skipped: bool } when a fullscreen video ends or is skipped,
so the playFullscreen promise resolves.
| Method | Key payload fields | Returns |
|---|---|---|
voxel.uploadChunk |
cx, cz (chunk coords), columns (Array of compact column records), chunkSize, seaLevel, atlasUrl, atlasTileSize, atlasTilesPerRow, trees (Array), meshMin/meshMax (mesh bounds override) |
{ ok } |
The compact column record format is documented in docs/GODOT_VOXEL_PLAN.md.
The WAD bridge exposes the WAD directory and raw lump data to JS for parsing
by runtime/wad.js. The C++ side handles file I/O; JS handles all parsing.
| Method | Key payload fields | Returns |
|---|---|---|
wad.load |
path (Godot resource path, e.g. res://assets/freedoom1.wad) |
{ handle, tag, directory[], path, size } |
wad.readLump |
handle, filepos, size |
{ bytes: Array, size } |
wad.destroy |
handle |
{ ok } |
wad.load returns a directory array of { name, filepos, size } objects
for every lump. Carts call wad.readLump to fetch individual lump byte
arrays, which are then decoded by WADLoader in JS.
Overlay commands draw on a CanvasLayer rendered above the 3D scene.
Coordinates are in the cart's 640×360 virtual canvas space.
| Method | Key payload fields | Returns |
|---|---|---|
overlay.cls |
r, g, b, a (clear color, default transparent) |
{ ok } |
overlay.pset |
x, y, color ({r,g,b,a}) |
{ ok } |
overlay.rect |
x, y, w, h, color ({r,g,b,a}), fill (bool, default true) |
{ ok } |
overlay.line |
x1, y1, x2, y2, color, width |
{ ok } |
overlay.circle |
x, y, r, color, fill |
{ ok } |
overlay.text |
x, y, text, color, size (font size px) |
{ ok } |
overlay.image |
texture/handle (texture handle), x, y, w, h, color ({r,g,b,a}, optional modulate) |
{ ok } |
overlay.batch |
ops: Array of individual overlay command objects (each has type + fields above) |
{ ok } |
overlay.batch is the preferred path for HUD-heavy carts; it amortises
the JS→C++ call overhead to a single round-trip per frame. Image ops use
the compact array form ['image', textureHandle, x, y, w, h, color].
Godot drives the cart lifecycle through Nova64Host node callbacks:
load_cart(res_path) — evaluates the cart ES module, caches init,
update, draw exports. Returns false if the module has no
recognisable lifecycle exports.cart_init() — calls the cart's init() export once.cart_update(delta) — calls update(dt) every _process frame.cart_draw() — calls draw() every frame after cart_update.The host GDScript (scripts/nova64_host.gd) wires these to
Node._process(delta).
The table below summarises supported and unsupported features compared to the Three.js browser backend.
| Feature | Three.js | Babylon.js | Godot |
|---|---|---|---|
| PBR materials | ✅ | ✅ | ✅ |
| Texture upload from pixel data | ✅ | ✅ | ✅ |
| Instanced meshes | ✅ | ✅ | ✅ |
| GPU particles | ✅ | ✅ | ✅ |
| Directional / point / spot lights | ✅ | ✅ | ✅ |
| Shadows | ✅ | ✅ | ✅ (default on) |
| Fog | ✅ | ✅ | ✅ |
| GLTF model loading | ✅ | ✅ | ✅ |
| VOX model loading | ✅ | ✅ | ✅ |
| WAD loading | ✅ | ✅ | ✅ |
| Voxel chunk meshing | ✅ | ✅ | ✅ (native C++ greedy mesher) |
| 2D overlay (HUD + image blits) | ✅ | ✅ | ✅ |
| Gamepad input | ✅ | ✅ | ✅ |
| Mouse look / pointer lock | ✅ | ✅ | ⚠️ partial (no browser pointer-lock API) |
| Audio | ✅ | ✅ | ✅ |
| Post-processing (bloom, SSAO) | ✅ | ✅ | ✅ (via env.set) |
Video texture on mesh (nova64.video) |
✅ | ✅ | 🟡 contract defined, host impl pending |
| Full-screen video playback | ✅ | ✅ | 🟡 contract defined, host impl pending |
| TSL / custom shaders | ✅ Three-only | ❌ | ❌ |
| WebXR / VR / AR | ✅ | ✅ | ❌ (Godot XR is separate and not yet bridged) |
| canvas2D textures (HTMLCanvas) | ✅ | ✅ | ❌ (no DOM; use texture.createFromImage instead) |
| Drag-and-drop WAD file loading | ✅ (via drop event) | ✅ | ❌ (use nova64.wad.load(res://)) |
The cart-facing shim is a hand-maintained second copy of runtime/, so it can also
diverge by accident. The semantics that are easy to invert — directional-light
orientation, createMaterial(kind) shading models, and WAD collision geometry — and
the pnpm test:godot:parity gate that pins them are documented in
GODOT_PARITY.md.
The deliberate divergences:
Input.mouse_mode = MOUSE_MODE_CAPTURED. The shim translates this but
raw mouse delta comes from Godot InputEventMouseMotion, not browser
movementX/Y. High-sensitivity mouse carts may need tuning.document.createElement('canvas') for texture generation must detect
Godot mode and use nova64.draw or texture.createFromImage instead.window.addEventListener, document.addEventListener,
fetch, XMLHttpRequest, and similar browser APIs are not available.
The shim guards the most common drag-and-drop and file-picker paths.All failed commands return:
{ "error": "<error_code>", "message": "<human_readable>" }
Common error codes:
| Code | Meaning |
|---|---|
unsupported_method |
Method not in the whitelist |
missing_<field> |
Required payload field absent |
invalid_<kind>_handle |
Handle does not resolve to expected type |
file_not_found |
Resource path not accessible |
wad_bad_magic |
File is not a valid IWAD/PWAD |
wad_lump_oob |
filepos + size exceeds WAD buffer |
| File | Purpose |
|---|---|
nova64-godot/godot_project/shim/nova64-compat.js |
Cart-facing nova64.* API for the QuickJS host (hand-ported from runtime/) |
nova64-godot/gdextension/src/bridge.cpp |
call_bridge dispatch and all _cmd_* implementations |
nova64-godot/gdextension/src/bridge.h |
Nova64Host class declaration, lifecycle method signatures |
nova64-godot/gdextension/src/handles.cpp |
HandleTable — allocates and type-checks all opaque handles |
nova64-godot/godot_project/shim/nova64-compat.js |
JS shim: translates cart API calls into engine.call pairs |
nova64-godot/godot_project/scripts/nova64_host.gd |
GDScript host wiring — cart load, process loop |
nova64-godot/godot_project/scripts/conformance_runner.gd |
Conformance harness that validates the contract surface |
tests/test-adapter-conformance.js |
Shared adapter conformance suite |