Skip to content

feat: implement TriggerArea component with Object Pool Manager - #945

Merged
kuruk-mm merged 20 commits into
mainfrom
feat/trigger-area
Dec 18, 2025
Merged

feat: implement TriggerArea component with Object Pool Manager#945
kuruk-mm merged 20 commits into
mainfrom
feat/trigger-area

Conversation

@kuruk-mm

@kuruk-mm kuruk-mm commented Dec 14, 2025

Copy link
Copy Markdown
Member

Summary

Implements TriggerArea (ID: 1060) and TriggerAreaResult (ID: 1061) components to detect when the player and other entities enter/exit defined zones.

Reference: ADR-258 | Protocol PR #306 | Closes #686

What Changed

New Components

  • TriggerArea: Defines a collision zone (box or sphere) attached to an entity
  • TriggerAreaResult: GrowOnlySet output with ENTER/STAY/EXIT events including position, rotation, and collision metadata

New Infrastructure

  • ObjectPool: Generic pooling system for reusing allocated resources
  • PhysicsAreaPool: Specialized pool for PhysicsServer3D RIDs (areas, box shapes, sphere shapes)
  • PoolManager: Centralized manager for all object pools with built-in leak detection

Why We Implemented It This Way

1. PhysicsServer3D Over Area3D Nodes

Instead of creating Area3D Godot nodes, we use PhysicsServer3D directly:

let area_rid = physics_server.area_create();
physics_server.area_set_space(area_rid, space_rid);
physics_server.area_add_shape(area_rid, shape_rid, transform, false);

Reasons:

  • Performance: No node tree overhead, no signals, no virtual calls
  • Control: Direct control over physics resources and callbacks
  • Memory: RIDs are lightweight handles; no GC pressure from node instantiation
  • Pooling: Easy to pool and reuse RIDs between entity lifecycle events

2. Monitor Callbacks for ENTER/EXIT Detection

We use area_set_monitor_callback for event-driven collision detection:

let body_callback = Callable::from_fn("trigger_body_monitor", move |args: &[&Variant]| {
    // status: 0 = BODY_ADDED (enter), 1 = BODY_REMOVED (exit)
    let status = args[0].to::<i64>();
    let instance_id = args[2].to::<i64>();
    // Queue event for processing in update_trigger_area()
});
physics_server.area_set_monitor_callback(area_rid, body_callback);

Reasons:

  • Event-driven: No per-frame polling needed for ENTER/EXIT detection
  • Efficient: PhysicsServer3D only calls us when state changes
  • Accurate: Events fire exactly when bodies enter/exit the area

STAY events are generated during update_trigger_area() for entities that remain inside.

3. RID Object Pooling

Physics resources (areas, shapes) are pooled instead of created/destroyed:

// Acquire from pool (reuses existing RID if available)
let area_rid = pool.acquire_area();

// Release back to pool when entity dies
pool.release_area(area_rid);
pool.release_box_shape(shape_rid);

Reasons:

  • Allocation avoidance: Creating PhysicsServer objects has overhead
  • Steady state: After warmup, no new allocations occur
  • Extensibility: Same pattern can be used for RenderingServer, AudioServer, etc.

4. Centralized PoolManager with Leak Detection

pub struct PoolManager {
    physics_area_pool: PhysicsAreaPool,
    // Future: rendering_pool, audio_pool, etc.
}

impl PoolManager {
    pub fn tick(&mut self) -> bool {
        // Periodic health check every 300 frames
        // Detects: in_use > created, unbalanced pools, suspicious growth
    }
}

Reasons:

  • Single reference: Pass one &mut PoolManager instead of multiple pools
  • Leak detection: Automatically warns if in_use grows without bound
  • Future-proof: Adding new pool types doesn't change function signatures
  • Debugging: debug_summary() and log_stats() for visibility

5. Entity-to-Entity Detection

Detects collisions between trigger areas and:

  • Player: Via CL_PLAYER collision layer (4) on CharacterBody3D
  • Scene entities: By checking dcl_entity_id metadata on colliders
// In monitor callback, check collider for DCL metadata
if object.has_meta("dcl_entity_id") {
    let entity_id = object.get_meta("dcl_entity_id").to::<i32>();
    // Queue ENTER/EXIT event
}

Decision Summary

Decision Alternative Considered Why Chosen
PhysicsServer3D Area3D nodes 10x+ faster, no node overhead
Monitor callbacks Per-frame polling Event-driven, no wasted queries
RID Pooling Create/free each time Eliminates allocation churn
PoolManager Pass pools individually Single reference, leak detection

Test Plan

How to test: Open decentraland://open?realm=kuruk.dcl.eth or go to kuruk.dcl.eth realm

  • Walk into trigger area → ENTER event fires
  • Stay inside trigger area → STAY events fire each frame
  • Walk out of trigger area → EXIT event fires
  • Delete entity with trigger area → RIDs returned to pool, no leak
  • Check pool stats after extended session → created stays stable

- Change verbose INFO logs to DEBUG for TriggerArea CREATE/DELETE
- Fix clippy cloned_ref_to_slice_refs warnings using std::slice::from_ref
- Add #[allow(clippy::too_many_arguments)] to build_trigger_result
- Apply cargo fmt formatting
@github-actions

github-actions Bot commented Dec 14, 2025

Copy link
Copy Markdown
Contributor

📊 Benchmark Report

📊 Baseline: Comparing against main branch at commit 78796be

Click to expand full benchmark report

Decentraland Godot Explorer - Benchmark Report

Generated: 2025-12-18_13-35-05

Total Tests: 5

📊 Comparison: vs main branch baseline

  • 🟢 = Improvement (better performance)
  • 🔴 = Regression (worse performance)
  • ⚪ = No significant change (<0.5%)

Table of Contents

  1. 1_Terms_and_Conditions
  2. 2_Lobby
  3. 3_Menu
  4. 4_Explorer_(72, -10)_Goerli_Plaza
  5. 4_Explorer_(-7, 0)_Genesis_Plaza

Summary Overview

Memory Metrics

Test Process RSS (MiB) Godot Static (MiB) GPU VRAM (MiB) Rust Heap (MiB) Deno Total (MiB)
1_Terms_and_Conditions 366.33 50.22 24.55 0.96 -0.00
2_Lobby 468.83 80.64 45.38 2.09 -0.00
3_Menu 1010.80 🟢 (-2.8%) 240.22 136.95 🔴 (+0.5%) 3.88 🔴 (+1.6%) -0.00
4_Explorer_(72, -10)_Goerli_Plaza 1471.61 🟢 (-1.1%) 305.20 175.89 7.05 🔴 (+1.0%) 21.18 🟢 (-8.2%)
4_Explorer_(-7, 0)_Genesis_Plaza 3073.00 940.43 552.17 7.70 10.84

Object Counts

Test Total Objects Nodes Resources Orphan Nodes
1_Terms_and_Conditions 2358 127 575 12
2_Lobby 4540 799 🔴 (+0.8%) 757 44
3_Menu 13236 4130 🔴 (+0.7%) 1017 44
4_Explorer_(72, -10)_Goerli_Plaza 19572 6526 1271 136
4_Explorer_(-7, 0)_Genesis_Plaza 28414 12306 1276 732

Rendering Metrics

Test FPS Draw Calls Primitives Objects in Frame
1_Terms_and_Conditions 30.00 14 376 139
2_Lobby 30.00 15 644 218
3_Menu 17.00 🟢 (+6.2%) 91 3874 373 🟢 (-1.8%)
4_Explorer_(72, -10)_Goerli_Plaza 5.00 418 🟢 (-5.0%) 155667 🟢 (-4.3%) 803 🟢 (-4.3%)
4_Explorer_(-7, 0)_Genesis_Plaza 3.0 578 759506 747

Resource Analysis

Test Meshes Materials Mesh RIDs Material RIDs Dedup Potential
1_Terms_and_Conditions 0 0 0 0 0
2_Lobby 0 0 0 0 0
3_Menu 0 0 0 0 0
4_Explorer_(72, -10)_Goerli_Plaza 274 276 274 13 0
4_Explorer_(-7, 0)_Genesis_Plaza 329 568 246 213 0

Detailed Test Results

Test 1: 1_Terms_and_Conditions

Benchmark Report: 1_Terms_and_Conditions

Timestamp: 2025-12-18_13-32-15
Location: UI Scene


Memory Metrics

Metric Value
Process Memory Usage (RSS) 366.33 MiB
Godot Static Memory 50.22 MiB
Godot Peak Memory 54.93 MiB
GPU Video RAM 24.55 MiB
GPU Texture Memory 17.27 MiB
GPU Buffer Memory 7.28 MiB
Rust Heap Usage 0.96 MiB
Rust Total Allocated 2.29 MiB

Object Counts

Metric Count
Total Objects 2358
Resources 575
Nodes 127
Orphan Nodes 12

Rendering Metrics

Metric Value
FPS 30.00
Draw Calls per Frame 14
Primitives per Frame 376
Objects per Frame 139

Test 2: 2_Lobby

Benchmark Report: 2_Lobby

Timestamp: 2025-12-18_13-32-21
Location: UI Scene


Memory Metrics

Metric Value
Process Memory Usage (RSS) 468.83 MiB
Godot Static Memory 80.64 MiB
Godot Peak Memory 112.02 MiB
GPU Video RAM 45.38 MiB
GPU Texture Memory 37.32 MiB
GPU Buffer Memory 8.06 MiB
Rust Heap Usage 2.09 MiB
Rust Total Allocated 20.47 MiB

Object Counts

Metric Count
Total Objects 4540
Resources 757
Nodes 799 🔴 (+0.8%)
Orphan Nodes 44

Rendering Metrics

Metric Value
FPS 30.00
Draw Calls per Frame 15
Primitives per Frame 644
Objects per Frame 218

Test 3: 3_Menu

Benchmark Report: 3_Menu

Timestamp: 2025-12-18_13-32-28
Location: UI Scene


Memory Metrics

Metric Value
Process Memory Usage (RSS) 1010.80 🟢 (-2.8%) MiB
Godot Static Memory 240.22 MiB
Godot Peak Memory 248.23 🔴 (+2.2%) MiB
GPU Video RAM 136.95 🔴 (+0.5%) MiB
GPU Texture Memory 126.23 🔴 (+0.6%) MiB
GPU Buffer Memory 10.72 MiB
Rust Heap Usage 3.88 🔴 (+1.6%) MiB
Rust Total Allocated 269.67 🔴 (+2.7%) MiB

Object Counts

Metric Count
Total Objects 13236
Resources 1017
Nodes 4130 🔴 (+0.7%)
Orphan Nodes 44

Rendering Metrics

Metric Value
FPS 17.00 🟢 (+6.2%)
Draw Calls per Frame 91
Primitives per Frame 3874
Objects per Frame 373 🟢 (-1.8%)

Test 4: 4_Explorer_(72, -10)_Goerli_Plaza

Benchmark Report: 4_Explorer_(72, -10)_Goerli_Plaza

Timestamp: 2025-12-18_13-32-58
Location: (72, -10)
Realm: https://sdk-team-cdn.decentraland.org/ipfs/goerli-plaza-main-latest


Memory Metrics

Metric Value
Process Memory Usage (RSS) 1471.61 🟢 (-1.1%) MiB
Godot Static Memory 305.20 MiB
Godot Peak Memory 307.00 MiB
GPU Video RAM 175.89 MiB
GPU Texture Memory 159.72 MiB
GPU Buffer Memory 16.17 MiB
Rust Heap Usage 7.05 🔴 (+1.0%) MiB
Rust Total Allocated 358.35 🔴 (+1.5%) MiB
Deno/V8 Total Memory 21.18 🟢 (-8.2%) MiB
Deno Active Scenes 1
Deno Avg per Scene 21.18 🟢 (-8.2%) MiB

Object Counts

Metric Count
Total Objects 19572
Resources 1271
Nodes 6526
Orphan Nodes 136

Rendering Metrics

Metric Value
FPS 5.00
Draw Calls per Frame 418 🟢 (-5.0%)
Primitives per Frame 155667 🟢 (-4.3%)
Objects per Frame 803 🟢 (-4.3%)

Resource Analysis

Metric Value
Total Mesh References 274
Total Material References 276
Unique Mesh RIDs 274
Unique Material RIDs 13
Hashed Mesh Count 0
Potential Deduplication 0 (0.0% savings)

Test 5: 4_Explorer_(-7, 0)_Genesis_Plaza

Benchmark Report: 4_Explorer_(-7, 0)_Genesis_Plaza

Timestamp: 2025-12-18_13-34-54
Location: (-7, 0)
Realm: https://realm-provider-ea.decentraland.org/main


Memory Metrics

Metric Value
Process Memory Usage (RSS) 3073.00 MiB (3.00 GiB)
Godot Static Memory 940.43 MiB
Godot Peak Memory 999.59 MiB
GPU Video RAM 552.17 MiB
GPU Texture Memory 463.35 MiB
GPU Buffer Memory 88.82 MiB
Rust Heap Usage 7.70 MiB
Rust Total Allocated 937.73 MiB
Deno/V8 Total Memory 10.84 MiB
Deno Active Scenes 1
Deno Avg per Scene 10.84 MiB

Object Counts

Metric Count
Total Objects 28414
Resources 1276
Nodes 12306
Orphan Nodes 732

Rendering Metrics

Metric Value
FPS 3.0
Draw Calls per Frame 578
Primitives per Frame 759506
Objects per Frame 747

Resource Analysis

Metric Value
Total Mesh References 329
Total Material References 568
Unique Mesh RIDs 246
Unique Material RIDs 213
Hashed Mesh Count 0
Potential Deduplication 0 (0.0% savings)


📋 Logs & Artifacts

  • 📊 CSV Data: benchmark_report.csv - Raw benchmark data in S3
  • 🔧 Full Logs: benchmark_run.log - Complete benchmark run output (build + execution)
  • 🌐 Workflow Run: View full logs
  • 📦 Download All: Get the benchmark-report artifact from the workflow run

🔄 Updated: 2025-12-18 13:35:09 UTC

@kuruk-mm

This comment was marked as outdated.

pool manager potential leak thread as an error
- Add TriggerDetector collision to avatars for trigger area detection
- Track avatar entity IDs via metadata (dcl_entity_id, dcl_scene_id)
- Implement scene-awareness: only fire events for entities in active scene
- Separate physical state (entities_inside) from logical state (entities_entered)
- Generate synthetic ENTER/EXIT events when entities change scenes while
  physically inside trigger areas
- Query avatar current scene via metadata to handle remote avatar scene changes
@kuruk-mm
kuruk-mm force-pushed the feat/trigger-area branch 2 times, most recently from 7a73e4c to b7812a7 Compare December 17, 2025 01:15
- Cache avatar scene info in AvatarTriggerInfo struct to avoid per-frame metadata queries
- Batch metadata queries across all trigger areas (query each avatar once, not per-area)
- Only query metadata when cache indicates potential state change:
  - If cache says avatar in scene: query to verify they haven't left
  - If cache says avatar not in scene: query to check if they joined
  - Skip query entirely when cache matches expected state
…isable

Replace complex state tracking and per-frame polling with simple
physics enable/disable based on player scene.

When player leaves a parcel scene:
- Generate EXIT for all entities inside trigger areas
- Disable physics monitoring (area_set_monitor_callback invalid)

When player enters a parcel scene:
- Re-enable physics monitoring
- PhysicsServer3D auto-fires ENTERs for overlapping bodies

Removed (~460 lines):
- AvatarTriggerInfo struct with last_known_scene cache
- entities_entered HashSet (dual state tracking)
- sync_entity_states() function (~190 lines of polling)
- get_avatar_current_scene() metadata query function
- Scene-awareness checks in process_callback_events()
- _on_avatar_scene_changed callback in avatar.gd

Added (~186 lines):
- check_scene_active() function for enable/disable logic
- is_active flag on TriggerAreaInstance
- last_player_scene_id field on Scene

Result: ~26% code reduction, simpler mental model, no per-frame
metadata queries, more responsive scene transitions.
Replace partition() with HashMap<SceneId, Vec<PendingTriggerEvent>> for
O(1) per-scene drain instead of O(E_total) scanning all events.

@leanmendoza leanmendoza left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well done!

// scene_id=-1 means this is a remote avatar (not a scene NPC)
// NOTE: This must be called AFTER add_child so that _ready() has been called
// and the @onready trigger_detector variable is initialized
new_avatar.call(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question here: as far i understood we won't be setting up triggers for non-primary players nor npc avatar. should we comment this line?

Scene-spawned AvatarShapes (dcl_scene_id >= 0) are now filtered out.
Only local player and remote avatars (dcl_scene_id == -1) trigger events.
AvatarShapes now have their trigger_detector node freed in avatar.gd
when setup_trigger_detection is called with a scene_id >= 0.
This prevents scene NPCs from triggering area events.
- Remove dcl_scene_id from avatar.gd (not needed anymore)
- Simplify setup_trigger_detection to only take entity_id
- AvatarShapes (skip_process=true) have trigger_detector freed in _ready()
- Don't call setup_trigger_detection for AvatarShapes in avatar_shape.rs
- Simplify trigger_area.rs to not check dcl_scene_id for avatars
Instead of relying on skip_process, explicitly call remove_trigger_detection()
from avatar_shape.rs to free the trigger_detector node for scene NPCs.
@kuruk-mm
kuruk-mm merged commit fc9f96d into main Dec 18, 2025
8 checks passed
@kuruk-mm
kuruk-mm deleted the feat/trigger-area branch December 18, 2025 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement new Trigger area component

2 participants