Skip to content
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,8 @@ set(FABRIC_SIMULATION_SOURCE_FILES
src/recurse/simulation/EssenceAssigner.cc
src/recurse/simulation/ChangeVelocityTracker.cc
src/recurse/simulation/ProjectionRuleTable.cc
src/recurse/simulation/WorldRuleEngine.cc
src/recurse/simulation/TransformationPass.cc
)

# Utils library components
Expand Down
5 changes: 3 additions & 2 deletions include/recurse/simulation/MaterialRegistry.hh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class MaterialRegistry {
MaterialRegistry();

const MaterialDef& get(MaterialId id) const {
assert(id < material_ids::COUNT && "MaterialId out of range");
assert(id < material_ids::REGISTRY_SIZE && "MaterialId out of range");
return materials_[id];
}

Expand All @@ -20,9 +20,10 @@ class MaterialRegistry {
}

MaterialId count() const { return material_ids::COUNT; }
MaterialId registrySize() const { return material_ids::REGISTRY_SIZE; }

private:
std::array<MaterialDef, material_ids::COUNT> materials_{};
std::array<MaterialDef, material_ids::REGISTRY_SIZE> materials_{};
};

} // namespace recurse::simulation
59 changes: 59 additions & 0 deletions include/recurse/simulation/TransformationPass.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#pragma once
#include "fabric/platform/JobScheduler.hh"
#include "recurse/simulation/ChunkActivityTracker.hh"
#include "recurse/simulation/GhostCells.hh"
#include "recurse/simulation/MaterialRegistry.hh"
#include "recurse/simulation/SimulationGrid.hh"
#include "recurse/simulation/WorldRuleEngine.hh"
#include <atomic>
#include <cstdint>
#include <random>
#include <vector>

namespace recurse::simulation {

struct ActiveChunkEntry; // forward declare

struct SubRegionActivation {
ChunkCoord pos;
int lx, ly, lz;
};

class TransformationPass {
public:
struct Config {
float diffusionRate = 0.25f;
int maxTransformsPerChunk = K_MAX_TRANSFORMS_PER_CHUNK;
};

TransformationPass(const WorldRuleEngine& rules, const MaterialRegistry& registry, SimulationGrid& grid,
const GhostCellManager& ghosts, ChunkActivityTracker& tracker);

/// Execute Phase 3c across all active chunks.
void execute(const std::vector<ActiveChunkEntry>& active, fabric::JobScheduler& scheduler, int64_t worldSeed,
uint64_t frameIndex);

/// Per-chunk execution (single-threaded; flushes activations to tracker directly).
void executeChunk(ChunkCoord pos, std::mt19937& rng);

Config& config() { return config_; }

/// Stats from last execute() call.
int totalTransforms() const { return totalTransforms_.load(std::memory_order_relaxed); }

private:
const WorldRuleEngine& rules_;
const MaterialRegistry& registry_;
SimulationGrid& grid_;
const GhostCellManager& ghosts_;
ChunkActivityTracker& tracker_;
Config config_;
std::atomic<int> totalTransforms_{0};

void thermalKernel(ChunkCoord pos);
void executeChunk(ChunkCoord pos, std::mt19937& rng, std::vector<SubRegionActivation>& activations);
int ruleEvaluation(ChunkCoord pos, std::mt19937& rng, std::vector<SubRegionActivation>& activations);
VoxelCell readCell(ChunkCoord pos, int lx, int ly, int lz) const;
};

} // namespace recurse::simulation
14 changes: 11 additions & 3 deletions include/recurse/simulation/VoxelMaterial.hh
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,25 @@ inline constexpr MaterialId DIRT = 2;
inline constexpr MaterialId SAND = 3;
inline constexpr MaterialId WATER = 4;
inline constexpr MaterialId GRAVEL = 5;
inline constexpr MaterialId COUNT = 6;
inline constexpr MaterialId COUNT = 6; // Base materials (worldgen, meshing)

// Synthetic essences produced by transformation rules
inline constexpr MaterialId ICE = 6;
inline constexpr MaterialId GLASS = 10;
inline constexpr MaterialId MAGMA = 11;
inline constexpr MaterialId REGISTRY_SIZE = 12; // Must be > max known ID
} // namespace material_ids

/// Broad matter mode for a voxel cell.
/// Values 5-7 reserved for future use (Growth, Unstable, etc.).
/// Values 5-6 reserved for future use (Growth, Unstable, etc.).
/// Value 7 is the "unchanged" sentinel for WorldRule result fields.
enum class Phase : uint8_t {
Empty = 0,
Solid = 1,
Powder = 2,
Liquid = 3,
Gas = 4
Gas = 4,
Unchanged = 7
};

enum class MoveType : uint8_t {
Expand Down
8 changes: 8 additions & 0 deletions include/recurse/simulation/VoxelSimulationSystem.hh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include "recurse/simulation/MaterialRegistry.hh"
#include "recurse/simulation/ProjectionRuleTable.hh"
#include "recurse/simulation/SimulationGrid.hh"
#include "recurse/simulation/TransformationPass.hh"
#include "recurse/simulation/WorldRuleEngine.hh"
#include <bit>
#include <cstdint>
#include <vector>
Expand Down Expand Up @@ -69,6 +71,8 @@ class VoxelSimulationSystem {
ChunkActivityTracker tracker_;
GhostCellManager ghosts_;
FallingSandSystem sandSystem_;
WorldRuleEngine ruleEngine_;
TransformationPass transformPass_;
fabric::JobScheduler scheduler_;
uint64_t frameIndex_ = 0;
int64_t worldSeed_ = 0;
Expand All @@ -85,6 +89,10 @@ class VoxelSimulationSystem {
const FallingSandSystem& fallingSandSystem() const { return sandSystem_; }
GhostCellManager& ghostCellManager() { return ghosts_; }
const GhostCellManager& ghostCellManager() const { return ghosts_; }
WorldRuleEngine& ruleEngine() { return ruleEngine_; }
const WorldRuleEngine& ruleEngine() const { return ruleEngine_; }
TransformationPass& transformPass() { return transformPass_; }
const TransformationPass& transformPass() const { return transformPass_; }
int64_t worldSeed() const { return worldSeed_; }

ChangeVelocityTracker& velocityTracker();
Expand Down
69 changes: 69 additions & 0 deletions include/recurse/simulation/WorldRuleEngine.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#pragma once
#include "recurse/simulation/VoxelMaterial.hh"
#include <cstdint>
#include <span>
#include <vector>

namespace recurse::simulation {

/// Flat 14-byte rule for world simulation behaviors.
/// Locked design: Decision 2 (2026-03-22). No nested types.
/// Tag is plain uint8_t for debugging (0=Thermal, 1=Contact, 2=Gravity).
///
/// Sentinel conventions for result fields:
/// resultEssenceA/B = 255: leave essence unchanged
/// resultPhaseA/B = Phase::Unchanged: leave phase unchanged
/// resultTempA/B = 0: leave temperature unchanged (temp cannot be set to literal zero)
struct WorldRule {
uint8_t essenceIdxA; // self (255 = wildcard)
uint8_t essenceIdxB; // neighbor (255 = self-transform)
Phase requiredPhaseA{Phase::Unchanged};
uint8_t temperatureMin{0};
uint8_t temperatureMax{255};
uint8_t resultEssenceA{255}; // 255 = unchanged
uint8_t resultEssenceB{255}; // 255 = unchanged
Phase resultPhaseA{Phase::Unchanged};
Phase resultPhaseB{Phase::Unchanged};
uint8_t resultTempA{0}; // 0 = unchanged
uint8_t resultTempB{0}; // 0 = unchanged
uint8_t probability{255}; // 0-255, 255 = always fire
uint8_t priority{128}; // higher = evaluated first
uint8_t tag{0}; // system origin for debugging
};
static_assert(sizeof(WorldRule) == 14, "WorldRule must be exactly 14 bytes");

/// Configurable max transformations per chunk per tick.
inline constexpr int K_MAX_TRANSFORMS_PER_CHUNK = 64;

class WorldRuleEngine {
public:
WorldRuleEngine();

/// Add a rule to the engine. Rules are kept sorted by priority (descending).
void addRule(WorldRule rule);

/// Query matching rules for a cell pair at a given temperature.
/// Appends matching rules (sorted by priority, highest first) to caller-owned buffer.
/// selfEssence: essenceIdx of the cell being evaluated
/// neighborEssence: essenceIdx of the adjacent cell (255 for self-transform queries)
/// selfPhase: phase of the cell being evaluated
/// temperature: temperature byte of the cell
/// results: caller-owned output buffer (cleared then filled)
void query(uint8_t selfEssence, uint8_t neighborEssence, Phase selfPhase, uint8_t temperature,
std::vector<WorldRule>& results) const;

/// Number of registered rules.
size_t ruleCount() const;

/// Access to all rules (for testing/debugging).
std::span<const WorldRule> allRules() const;

private:
std::vector<WorldRule> rules_;

void sortByPriority();
bool matches(const WorldRule& rule, uint8_t selfEssence, uint8_t neighborEssence, Phase selfPhase,
uint8_t temperature) const;
};
Comment thread
mannie-exe marked this conversation as resolved.

} // namespace recurse::simulation
29 changes: 28 additions & 1 deletion src/recurse/simulation/MaterialRegistry.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ MaterialRegistry::MaterialRegistry() {
air.baseColor = 0x00000000;
air.meltPoint = 0;
air.boilPoint = 0;
air.thermalConductivity = 255; // fast conductor (convection)
air.thermalConductivity = 25; // poor conductor (insulator)

// Stone: heavy static block
auto& stone = materials_[material_ids::STONE];
Expand Down Expand Up @@ -82,6 +82,33 @@ MaterialRegistry::MaterialRegistry() {
gravel.meltPoint = 190;
gravel.boilPoint = 0;
gravel.thermalConductivity = 70; // moderate conductor

// ICE: frozen water, lighter than liquid water
auto& ice = materials_[material_ids::ICE];
ice.moveType = MoveType::Static;
ice.density = 90;
ice.baseColor = 0xFFD0E8FF;
ice.meltPoint = 91;
ice.boilPoint = 0;
ice.thermalConductivity = 100;

// GLASS: vitrified sand, good insulator
auto& glass = materials_[material_ids::GLASS];
glass.moveType = MoveType::Static;
glass.density = 160;
glass.baseColor = 0xFFA08040;
glass.meltPoint = 179;
glass.boilPoint = 0;
glass.thermalConductivity = 40;

// MAGMA: molten stone, heavy liquid
auto& magma = materials_[material_ids::MAGMA];
magma.moveType = MoveType::Liquid;
magma.density = 210;
magma.baseColor = 0xFFFF4400;
magma.meltPoint = 0;
magma.boilPoint = 0;
magma.thermalConductivity = 200;
}

} // namespace recurse::simulation
15 changes: 14 additions & 1 deletion src/recurse/simulation/ProjectionRuleTable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ void ProjectionRuleTable::setRule(uint8_t essenceIdx, Phase phase, const Project

void ProjectionRuleTable::populateFromRegistry(const MaterialRegistry& registry) {
// During migration, MaterialId maps 1:1 to essenceIdx for registered materials.
const MaterialId limit = std::min(registry.count(), static_cast<MaterialId>(K_MAX_ESSENCE));
const MaterialId limit = std::min(registry.registrySize(), static_cast<MaterialId>(K_MAX_ESSENCE));
for (MaterialId id = 0; id < limit; ++id) {
const auto& def = registry.get(id);

Expand Down Expand Up @@ -59,6 +59,19 @@ void ProjectionRuleTable::populateFromRegistry(const MaterialRegistry& registry)

setRule(static_cast<uint8_t>(id), phase, projected);
}

// Display-only overrides for synthetic essences.
// density, baseColor, and moveType are already set by the loop above.
// Only displayName and reductionTiebreak need manual values.
auto applyDisplayOverride = [&](uint8_t essenceIdx, Phase phase, std::string_view name, uint8_t tiebreak) {
size_t idx = index(essenceIdx, phase);
table_[idx].displayName = name;
table_[idx].reductionTiebreak = tiebreak;
};

applyDisplayOverride(material_ids::ICE, Phase::Solid, "ice", 110);
applyDisplayOverride(material_ids::GLASS, Phase::Solid, "glass", 130);
applyDisplayOverride(material_ids::MAGMA, Phase::Liquid, "magma", 200);
}

} // namespace recurse::simulation
Loading
Loading