-
Notifications
You must be signed in to change notification settings - Fork 0
Modules
The LplPlugin engine is organized into 20 independent static libraries (lpl-<name>), each with its own xmake.lua, include/lpl/<name>/ headers, and src/ implementations. The engine/ module serves as the top-level facade that aggregates all others; a separate samples/ tree holds example code.
Build target naming: Each module compiles to
lpl-<name>(e.g.,lpl-core,lpl-math,lpl-ecs). Code style: SOLID principles, Doxygen/**@documentation,.inlinline files, guard clauses, no inline comments.
Target: lpl-core — No dependencies
Platform types, assertions, compile-time utilities, and logging foundations. This is the leaf dependency of the entire module graph.
Target: lpl-math → lpl-core
Vec3, Quat, BoundaryBox, Fixed32, CORDIC trigonometry — all without external dependencies (no GLM).
Features:
- All operators annotated
__host__ __device__(LPL_HD) for CUDA compatibility - Deterministic fixed-point arithmetic (
Fixed32) for cross-platform physics validation - Custom CORDIC implementations for
sin/cos/atan2
API:
-
Vec3:+,-,*,/,+=,dot(),cross() -
Quat:identity(),*(Hamilton),rotate(Vec3) -
BoundaryBox:contains(Vec3)— inclusion test for migration
Target: lpl-memory → lpl-core
PinnedAllocator: STL allocator using cudaHostAlloc (mapped + portable) so that std::vector buffers are directly GPU-accessible via PCIe zero-copy.
Fallback: Without nvcc, uses standard malloc/free.
Usage: All SoA vectors in Partition use PinnedAllocator<T>:
std::vector<Vec3, PinnedAllocator<Vec3>> _positions[2];This lets the physics kernel obtain a device pointer via cudaHostGetDevicePointer() without any explicit host↔device copy.
Target: lpl-container → lpl-core, lpl-math
Lock-free hash map specialized for world chunk storage.
-
Contiguous pool: all
Partitionobjects live in a contiguous array (cache-friendly) -
64-bit atomic packed entries:
[Morton Key (42 bits) | Pool Index (22 bits)] - Max capacity: 2²² = 4M items (bounded by the 22-bit index)
- Open addressing: linear probing with tombstones
-
Slot recycling: free-index stack protected by a
SpinLock
| Operation | Guarantee | Mechanism |
|---|---|---|
get() |
Wait-Free | Atomic read |
insert() |
Lock-Free | Atomic CAS + SpinLock for pool |
remove() |
Lock-Free | Atomic TOMBSTONE + slot recycling |
forEach() |
Thread-Safe | Copy _activeSlots under lock |
Encode 2D/3D coordinates into scalar keys preserving spatial locality (Z-order curve).
-
encode2D(x, y)→ 32-bit key (world chunks), bit-interleave viapart1by1 -
encode3D(x, y, z)→ 63-bit key (octree), bit-interleave viapart1by2 - Bias for negative coordinates keeps everything positive before interleaving:
const uint64_t bias = 1ULL << 20; // 1,048,576
uint64_t ux = static_cast<uint64_t>(x) + bias; // -5 → 1,048,571
uint64_t uz = static_cast<uint64_t>(z) + bias;
return Morton::encode2D(ux, uz); // everything positiveTarget: lpl-concurrency → lpl-core
std::atomic_flag + _mm_pause() — RAII LocalGuard, ~100ns latency under short contention. One per chunk.
Fixed workers (hardware_concurrency()), supports enqueue (returns std::future) and enqueueDetached (fire-and-forget). Used by SystemScheduler and WorldPartition.
Target: lpl-ecs → lpl-core, lpl-math, lpl-memory, lpl-container, lpl-concurrency
Central registry — sparse set with generational IDs.
-
smartId = (generation << 14) | slot(18-bit gen + 14-bit slot) - O(1) lookup:
publicId → slot → chunkKey - Capacity: 10,000 simultaneous entities, 1,000,000 max public IDs
Per-chunk SoA container with double buffering, physics, and migration.
SoA Structure:
-
Hot data
[2](PinnedAllocator): positions, velocities, forces - Cold data: ids, rotations, masses, sizes, health, neuralControls
-
Metadata: adaptive sparse set
_idToLocal, BoundaryBox, SpinLock, FlatDynamicOctree
| Method | Description |
|---|---|
addEntity(snapshot) |
Thread-safe insertion into BOTH buffers |
removeEntityById(id, writeIdx) |
O(1) swap-and-pop on BOTH buffers |
physicsTick(dt, out, wIdx) |
Gravity + semi-implicit Euler + migration (CPU) |
syncBuffers(writeIdx) |
Copy hot data write→read (memcpy) |
setNeuralControl(id, ...) |
Update BCI data for an entity |
Physics (CPU fallback) runs in two passes to avoid double-ticking: a forward pass integrates gravity → acceleration → velocity → position (floor clamp y ≥ 0), then a backward pass does the bounds check and swap-and-pop of out-of-bounds entities (iterating backward so indices stay valid).
Idea — dirty-list tracking: track which entities the network actually modified (atomic bool per entity, or a LIFO of dirty indices) so the GPU recomputes only what changed instead of the whole chunk. Not exploited finely yet.
Full rebuild each frame (no incremental insertion). Internal radix sort (4 passes × 16-bit) on 3D Morton keys, recursive subdivision into 8 octants. Parameters: maxDepth = 8, leafCapacity = 32. Adaptive: brute-force N² for ≤32 entities, octree beyond.
Note: the octree lives in each
Partition, butupdateSpatialIndex()is not yet called automatically instep()— spatial queries must trigger a rebuild explicitly for now.
Target: lpl-physics → lpl-ecs, lpl-math, lpl-container
Main orchestrator — manages chunks, migration, entity registry, and global double buffering.
step(deltatime) Flow:
forEach chunk → physicsTick (CPU, 2 passes forward+backward)
Final phase: Re-insert migrating entities (getOrCreateChunk + addEntity + updateChunkKey)
GC: Remove empty chunks
- Minimum penetration axis → collision normal
- Newton impulse with restitution (
e=0.5) - Iterative solver (4 complete passes)
- Auto wake on collision
Target: lpl-net → lpl-core, lpl-math, lpl-memory, lpl-container, lpl-concurrency, lpl-ecs
Three modes (résolution automatique) :
-
Kernel driver (production):
open("/dev/lpl0")→mmap(LplSharedMemory)→ zero-copy RX/TX - Socket fallback (serveur dev): socket UDP standard
-
Socket forcé (
-DLPL_USE_SOCKET, client/Android): socket UDP uniquement
| Method | Description |
|---|---|
network_init() |
Open driver (or create socket) |
network_consume_packets(queue) |
Drain RX ring/socket → PacketQueue
|
send_connect/welcome/inputs/state() |
Typed send methods |
Thread-safe typed queues: ConnectEvent, WelcomeEvent, StateUpdateEvent, InputEvent.
Design note — decoupling: Network no longer knows about WorldPartition or InputManager. It only pushes typed events into the PacketQueue; the ECS systems (Systems.hpp) consume them via drain() and mutate the world. On the server, the constructor tries /dev/lpl0 first and transparently falls back to a UDP socket on port 7777 (_useKernelDriver records the active mode).
Handles client connections server-side. handleConnections() (drain ConnectEvent, skip duplicate IP+port, create player entity from _nextEntityId starting at 100, send MSG_WELCOME, register the endpoint) + broadcast_state() (serialize every entity, 32 bytes each, send MSG_STATE to each client).
Planned: move
SessionManagerto Read-Copy-Update (RCU) semantics so broadcasts read a stable session snapshot without locking out connections.
Factory functions (Systems:: namespace) returning SystemDescriptor for the scheduler:
-
NetworkReceiveSystem,SessionSystem,InputProcessingSystem,MovementSystem,PhysicsSystem,WelcomeSystem,StateReconciliationSystem,BroadcastSystem
Client-only systems (BCISystem, LocalInput, Spawn, InputSend, Camera, Render) live in the application, not here, because they pull in external libs (GLFW, Vulkan, BCI backends).
-
ServerMesh (
net/ServerMesh) — multi-server worlds where a chunk is the migratable unit; remaining work is entity serialization to a target node and a mesh heartbeat/registration protocol. -
Rollback netcode (
net/netcode/RollbackStrategy) — a circular buffer of serialized state snapshots enabling client-side rollback and reconciliation.
Target: lpl-gpu → lpl-core, lpl-math
CUDA lifecycle and physics kernel implementation. PhysicsKernel.cu handles gravity and semi-implicit Euler integration on SoA buffers, one thread per entity:
// gravity + semi-implicit Euler, one thread per entity
forces[idx] = Vec3{0.0f, -9.81f * masses[idx], 0.0f};
if (masses[idx] > 0.0001f)
velocities[idx] += forces[idx] * (1.0f / masses[idx]) * dt;
positions[idx] += velocities[idx] * dt;API: gpu_init() / gpu_cleanup() (CUDA lifecycle + timing events), launch_physics_kernel(...) (256 threads/block, no sync), gpu_sync() (cudaDeviceSynchronize() — a single barrier for all chunks), gpu_timer_start/stop() via cudaEvent. If __CUDACC__ is undefined, every function is an inline void no-op → transparent g++ compilation.
Ported:
PhysicsGPU.cufrom legacy →PhysicsKernel.cu(SoA float arrays).
Target: lpl-input → lpl-core, lpl-math
Unified input state per entity (keys, axes, neural). Used on both server (authoritative from network) and client (local from input devices + BCI).
| Method | Description |
|---|---|
setKeyState(entityId, key, pressed) |
Update key state |
setNeural(entityId, alpha, beta, conc, blink) |
Update neural data |
computeMovementVelocity(entityId, ...) |
WASD → velocity + neural modulation |
Neural speed scale: concentration [0..1] → scale [0.70x .. 1.30x]. Blink jump with rising-edge detection.
Target: lpl-render → lpl-shaders, lpl-core, lpl-math, lpl-memory (+ imgui, vulkan-hpp, vulkan-loader, stb, tinyobjloader, glm)
A renderer built around an IRenderer interface with two interchangeable backends and a cross-target parity guarantee (the software path and the Vulkan path produce the same image, checked by RenderParity):
- VulkanRenderer — device/instance/swapchain, graphics pipeline, ImGui (GLFW + Vulkan backend), texture and model loading (stb, tinyobjloader).
-
SoftwareRasterizer — dependency-free CPU rasterizer (its
Topologyuses only+,-,*,/and a hardwaresqrt), so a frame can be produced with no GPU at all.
Supporting pieces: Camera, Projection, Material, Pbr (physically-based shading), Lighting, Mesh, Instancing, Texture, Shader, AssetCache, a RayTracer, and Foveated rendering (higher resolution at the gaze centre — a VR/FullDive-oriented optimization). A kernel/ subtree keeps a rendering path usable from the freestanding side.
Note: the renderer is optional.
xmake f --renderer=ndisables Vulkan and GLFW for headless server builds; the engine then links withoutlpl-render.
Target: lpl-audio → lpl-core
Architecture stub for spatial audio integration. Not yet implemented.
Target: lpl-haptic → lpl-core
Architecture stub for haptic/vestibular feedback (GVS, tFUS). Not yet implemented.
Target: lpl-bci → lpl-core, lpl-math, lpl-input, lpl-container (+ eigen, liblsl, brainflow optional)
Full acquisition-to-metric pipeline. Organized into sub-directories:
| Sub-directory | Content |
|---|---|
src/source/ |
OpenBCI driver, BrainFlow, LSL, CSV sources |
src/dsp/ |
FFT, windowing, PSD extraction |
src/metric/ |
SignalMetrics (Schumacher), StabilityMetric |
src/math/ |
Covariance, Riemannian geometry |
src/stream/ |
LSL outlet |
src/openvibe/ |
OpenViBE box algorithm stubs |
tests/ |
24 unit tests (SignalMetrics + RiemannianGeometry) |
Key APIs:
- Serial capture: 33-byte packets, 8 channels, 250 Hz
- 24-bit ADS1299 parsing with sign extension
- FFT (256 pts, Hann window) → PSD per channel
- Concentration ratio (
Beta / (Alpha + Beta)) with EMA smoothing - Eye blink detection via thresholding
-
schumacher()—$R(t) = \frac{1}{N_{ch}} \sum_{i=1}^{N_{ch}} \int_{40}^{70} PSD_i(f,t), df$ — muscle tension -
integrale()— discrete PSD integration -
sliding_window_rms()— RMS over sliding window -
compute_baseline()— mean + σ calibration
-
compute_covariance()— SPD matrix with Bessel correction -
riemannian_distance()—$\delta_R = \sqrt{\sum_i \ln^2(\lambda_i)}$ — cognitive state change -
mahalanobis_distance()—$D_M$ anomaly detection - Jacobi eigenvalue decomposition — no external dependency
-
from_state()— normalized struct withmuscle_alertthreshold
Per-subject calibration: a ~30 s rest phase collects a baseline so metrics like Schumacher R(t) are normalized against the subject's own resting state (baseline_R, σ) instead of absolute values.
Idea — neuro-feedback loop: close the loop so the simulation reacts in real time to
NeuralMetrics(concentration, muscle alert).
Note:
BrainFlow,liblsland Eigen are now packaged in the official xmake-repo, so the externalBrainFlowSource/LslSource/OpenBciSourcesources can be enabled — no longer blocked on missing dependencies.
Target: lpl-serial → lpl-core, lpl-math, lpl-ecs, lpl-net, lpl-input
State serialization and deterministic replay (not the USB serial port — that lives in bci/source/serial/ now). Built around an ISerializable interface plus StateSnapshot (a serialized world state at a given tick):
- ReplayRecorder — serialize frames + snapshots to disk as the simulation runs.
- ReplayPlayer — seek to the closest tick by binary search over snapshots, then replay forward.
Because the simulation is deterministic (fixed-point maths, fixed tick), a recorded input+snapshot stream reproduces a run bit-for-bit — the basis for debugging, regression tests and the rollback netcode in net/.
Target: lpl-platform → lpl-core
Backend interfaces that let the engine run against different hosts (userspace Linux today, the freestanding kernel target tomorrow) behind one API: IPlatform, IClockBackend, IDisplayBackend, IGpuMemoryBackend, IInputBackend. Concrete backends live in platform/linux/ and platform/kernel/; swapping host means swapping a backend, not touching the engine.
Target: lpl-image → lpl-core
Dependency-free 2D imaging: Surface and Image buffers, Color, a Painter for software drawing, and a Codec for encode/decode. Used where a frame or texture must be produced without a GPU (software renderer output, tooling, headless capture).
Target: lpl-scene → lpl-core, lpl-math
Lightweight scene description: a Scene container and Transform2D nodes. Sits above the ECS/renderer to organize what gets drawn.
Target: lpl-bench → lpl-core
The measurement library behind lpl-benchmark: a Harness that times a callback, aggregates runs (mean, min, p99, variance, sample count) and classifies real-time budgets, plus SystemInfo for host/CPU details. Shared so any module can be micro-benchmarked the same way.
Target: lpl-engine → ALL other modules (lpl-render only when --renderer is on) + glfw
Top-level facade that depends on every other module. Provides the Core class — the single entry point for the engine.
Core owns by composition:
-
WorldPartition,Network,SystemScheduler,InputManager,PacketQueue,SessionManager
Game loops:
-
run(threaded)— fixed 60Hz server loop -
runVariableDt(computeDt, onPostLoop)— variable client loop
Lifecycle: Constructor → registerSystem() → buildSchedule() → run() → stop() → Destructor
ECS scheduler with automatic component dependency analysis (DAG) and two execution phases:
- PreSwap: mutation of write buffer (network, inputs, physics)
- PostSwap: reading of stable read buffer (broadcast, render, camera)
Systems declaring the same write component are serialized; reads are parallelized. Execution proceeds by ordered stages within each phase, with a ThreadPool running same-stage systems concurrently and a std::latch as the stage barrier.
The intended full client pipeline (app-level, some systems pull in GLFW/Vulkan/BCI) reads: NetworkReceive → Welcome → StateReconciliation → BCI → LocalInput → Spawn → Movement → Physics → InputSend (PreSwap), then Camera → Render (PostSwap). The server pipeline is NetworkReceive → Session → InputProcessing → Movement → Physics (PreSwap) then Broadcast (PostSwap).
Not an xmake target — compiled separately via kernel build system.
Bidirectional zero-copy network gateway:
RX Path: Netfilter hook NF_INET_PRE_ROUTING intercepts UDP on port 7777 before routing → skb_copy_bits() extracts the payload into an RxPacket (with src_ip/src_port) → the packet is NF_DROPped (never reaches the standard stack). Allocation in hook context uses GFP_ATOMIC.
TX Path: kernel thread lpl_tx_worker polls the TX ring, sleeps on a wait queue (tx_wq) until woken by ioctl(LPL_IOC_KICK_TX), then sends each TxPacket via kernel_sendmsg() on a kernel UDP socket — zero syscall overhead from userspace beyond the ioctl wake.
Shared Memory (LplSharedMemory):
-
mmap()on char device/dev/lpl0 -
vmalloc_user()allocation (contiguous, user-mappable virtual pages) -
RxRingBuffer rx(4096 slots) +TxRingBuffer tx(4096 slots) - Atomic
head/tail+ cache-line padding; userspace crosses the boundary withsmp_load_acquire/smp_store_release(via__atomicbuiltins)
Shared structures between kernel (pure C) and userspace (C++23). The header must stay pure-C compatible for the kernel:
| Structure | Description |
|---|---|
RingHeader |
head + tail (uint32_t) + 6× padding for cache-line alignment |
RxPacket |
src_ip (4B) + src_port (2B) + length (2B) + data[256] — inbound |
TxPacket |
dst_ip (4B) + dst_port (2B) + length (2B) + data[256] — outbound |
LplSharedMemory |
RxRingBuffer rx + TxRingBuffer tx (single mmap region) |
ComponentID |
TRANSFORM(1), HEALTH(2), VELOCITY(3), MASS(4), SIZE(5), NEURAL(6)
|
ioctl: LPL_IOC_KICK_TX — wakes the kernel TX thread to drain the ring.
Messages: MSG_CONNECT (0x10), MSG_WELCOME (0x11), MSG_INPUT (0x12), MSG_STATE (0x13)
← Tech Stack & Build | Next: Architectural Decisions →
LplPlugin — Zero-Copy Real-Time VR Engine | Author: MasterLaplace | License: GPL-3.0 | Source Code
This project is a marathon, not a sprint.