Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Environment Processing Unit (EPU)

The Environment Processing Unit (EPU) is ZX’s instruction-based procedural environment system. You provide a packed 128-byte configuration (8 × 128-bit instructions) and use epu_set(config_ptr) + draw_epu() to:

  • Render the environment background
  • Drive ambient + reflection lighting for lit materials (computed on the GPU)

For canonical ABI docs, see nethercore/include/zx.rs. For the opcode catalog/spec, see nethercore-design/specs/epu-feature-catalog.md.


FFI

environment_index

Select which EPU environment (env_id) subsequent draw calls will sample for ambient + reflections.

/// Select the EPU environment ID for subsequent draws (0..255).
fn environment_index(env_id: u32);

epu_set

Store the environment config for the currently selected environment_index(...) (no background draw).

To configure multiple environments in the same frame, call environment_index(env_id) then epu_set(config_ptr) for each env_id you use.

/// Store the EPU config for the current environment_index(...).
///
/// config_ptr points to 16 u64 values (128 bytes):
/// 8 instructions × (hi u64, lo u64)
fn epu_set(config_ptr: *const u64);

draw_epu

Draw the environment background for the current viewport/pass.

/// Draw the EPU background for the current viewport/pass.
fn draw_epu();

Call draw_epu() after your 3D geometry so the environment only fills background pixels.

Notes:

  • For split-screen, set viewport(...) and call draw_epu() per viewport.
  • The EPU compute pass runs automatically before rendering.
  • Ambient lighting is computed and applied entirely on the GPU; there is no CPU ambient query.
  • epu_set(...) stores a config for the currently selected environment_index(...).

Configuration Layout

Each environment is exactly 8 × 128-bit instructions (128 bytes total). In memory, that’s 16 u64 values laid out as 8 [hi, lo] pairs.

SlotKindRecommended Use
0–3BoundsAny bounds opcode (0x01..0x07). Common convention: start with RAMP to explicitly set up/ceil/floor/softness, then add SECTOR/SILHOUETTE/etc., but it is not required.
4–7RadianceDECAL / GRID / SCATTER / FLOW + radiance ops (0x0C..0x13)

Instruction Bit Layout (128-bit)

Each instruction is packed as two u64 values:

High Word (bits 127..64)

bits 127..123: opcode     (5)  - Which algorithm to run
bits 122..120: region     (3)  - Bitfield: SKY=0b100, WALLS=0b010, FLOOR=0b001
bits 119..117: blend      (3)  - How to combine layer output (8 modes)
bits 116..112: meta5      (5)  - (domain_id<<3)|variant_id; use 0 when unused
bits 111..88:  color_a    (24) - RGB24 primary color
bits 87..64:   color_b    (24) - RGB24 secondary color

Low Word (bits 63..0)

bits 63..56:   intensity  (8)  - Layer brightness
bits 55..48:   param_a    (8)  - Opcode-specific
bits 47..40:   param_b    (8)  - Opcode-specific
bits 39..32:   param_c    (8)  - Opcode-specific
bits 31..24:   param_d    (8)  - Opcode-specific
bits 23..8:    direction  (16) - Octahedral-encoded direction (u8,u8)
bits 7..4:     alpha_a    (4)  - color_a alpha (0=transparent, 15=opaque)
bits 3..0:     alpha_b    (4)  - color_b alpha (0=transparent, 15=opaque)

Determinism (No Host Time)

The EPU has no host-managed time input. Any temporal variation (scrolling, pulsing, drifting, twinkling, etc.) must be driven explicitly by the game by changing instruction parameters as part of deterministic simulation.

In practice this usually means incrementing an opcode-specific phase parameter (often param_d, see the opcode catalog) each frame: 0, 1, 2, 3, … (wrapping at 255), and re-calling epu_set(...) with the updated config.


Opcode Map (current shaders)

This is the opcode number. Some opcodes use meta5 for domain/variant selection; when unused, set meta5 = 0.

CodeNameNotes
0x00NOPDisable layer
0x01RAMPBounds gradient
0x02SECTORBounds modifier
0x03SILHOUETTEBounds modifier
0x04SPLITBounds
0x05CELLBounds
0x06PATCHESBounds
0x07APERTUREBounds
0x08DECALRadiance
0x09GRIDRadiance
0x0ASCATTERRadiance
0x0BFLOWRadiance
0x0CTRACERadiance
0x0DVEILRadiance
0x0EATMOSPHERERadiance
0x0FPLANERadiance
0x10CELESTIALRadiance
0x11PORTALRadiance
0x12LOBERadiance
0x13BANDRadiance

For full per-opcode packing/algorithm details, see:

  • nethercore-design/specs/epu-feature-catalog.md
  • nethercore/nethercore-zx/shaders/epu/

Region Mask (3-bit bitfield)

Regions are combinable using bitwise OR:

ValueBinaryNameMeaning
70b111ALLApply to sky + walls + floor
40b100SKYSky/ceiling only
20b010WALLSWall/horizon belt only
10b001FLOORFloor/ground only
60b110SKY_WALLSSky + walls
50b101SKY_FLOORSky + floor
30b011WALLS_FLOORWalls + floor
00b000NONELayer disabled

The region mask is consumed by feature/radiance opcodes: their contribution is multiplied by region_weight(current_regions, mask).

current_regions comes from the most recent bounds opcode; every bounds opcode outputs updated RegionWeights for subsequent layers. (Bounds opcodes do not use the region mask.)


Blend Modes (3-bit, 8 modes)

ValueNameFormula
0ADDdst + src * a
1MULTIPLYdst * mix(1, src, a)
2MAXmax(dst, src * a)
3LERPmix(dst, src, a)
4SCREEN1 - (1-dst)*(1-src*a)
5HSV_MODHSV shift dst by src
6MINmin(dst, src * a)
7OVERLAYPhotoshop-style overlay

meta5

The 5-bit meta5 field (hi bits 116..112) is interpreted as:

  • meta5 = (domain_id << 3) | variant_id
  • domain_id = (meta5 >> 3) & 0b11
  • variant_id = meta5 & 0b111

Quick Start

The easiest reference implementation is the EPU showcase presets:

  • nethercore/examples/3-inspectors/epu-showcase/src/presets.rs
  • nethercore/examples/3-inspectors/epu-showcase/src/constants.rs
// 8 x [hi, lo]
static ENV: [[u64; 2]; 8] = [
    [0, 0], [0, 0], [0, 0], [0, 0],
    [0, 0], [0, 0], [0, 0], [0, 0],
];

fn render() {
    unsafe {
        epu_set(ENV.as_ptr().cast());
        // ... draw scene geometry
        draw_epu();
    }
}

See Also