feat: scene content stats — debug widget, creator Scene Stats panel & MCP tools - #9457
Conversation
Adds per-scene content statistics (entities, triangles, meshes/bodies, geometries, materials, textures, colliders, runtime content size and external content) to the "Current scene" debug widget, shown as current / maxcap (pct%) colored green/yellow/red against hardcoded caps derived from the scene's parcel count. Counting runs in a new scene-world system (SceneContentStatsSystem) gated by a demand flag the widget sets only while expanded, so it costs a single bool check per frame when the debug panel is closed or disabled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Windows and Mac build successful in Unity Cloud! You can find a link to the downloadable artifact below. |
|
Warnings not reduced: 13729 => 13731 — remove at least 3 warnings to merge. Warnings/errors in files changed by this PR (83) |
|
|
Deployed-size validation belongs to deploy time (catalyst) and SDK tooling; the runtime-memory measurement compared a different quantity against the 15MB/parcel budget and showed red on deploy-compliant scenes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep the five caps that exist on the official scene-limitations page (triangles, entities, bodies, materials, textures) and show geometries, colliders and external content as plain counts - their caps were project-invented (geometries only existed in legacy SDK6 docs). The docs define the limits as soft, so exceeding one now renders yellow instead of red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a third sidebar button to the creator-facing scene debug menu (local scene development / --scene-console) that opens a Scene Metrics panel showing the same content stats as the Current Scene debug widget. Formatting and caps move to a shared SceneContentStatsFormatter in DCL.Profiling so both consumers render identical rows, and the collection demand flag is split per consumer so either UI can drive the scene-world counting system independently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds get_scene_content_stats to the embedded MCP server so agents and scripts can read the same numbers as the Current Scene debug widget and the scene metrics panel as structured JSON, including the documented soft-limit caps for the scene's parcel count. The tool sets its own demand flag and waits for the scene world to complete a counting pass (CollectionCount stamp), so it returns fresh values even while every stats UI is closed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
get_scene_content_breakdown ranks the current scene's rendered content by triangles, grouped by GLTF source (plus one aggregate row for primitive meshes), reporting instances, renderers and share of the scene total. The stats system fills the breakdown during a normal counting pass when a one-shot flag is set, so it costs nothing unless requested. Answers 'what should I optimize', not just 'over budget'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each breakdown entry now reports the source's unique material count and a draw-call estimate (material slots across renderers, pre-batching), and get_scene_content_breakdown gains a sortBy argument (triangles/materials/drawCalls) so an agent can answer 'where do my 2,000 materials come from' directly. Primitive meshes flow through the same grouping path instead of dedicated accumulators. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Current scene widget keeps the runtime tick/FPS/traffic metrics; the new Scene content widget owns the content stat rows and is the only debug-panel consumer that triggers the counting pass, so watching performance no longer pays for content collection (and vice versa). Also halves the collection frequency (30 -> 60 frame cooldown), makes every consumer re-format only when a new pass lands (CollectionCount tracking), and raises the MCP tool timeouts to survive the longer cadence on low-FPS scenes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every breakdown pass now also records each source's visible subset - renderers that passed culling for the current camera (Renderer.isVisible, shadow casters included) with their triangles and material slots - and get_scene_content_breakdown reports them per entry and as view totals, with sortBy=visibleTriangles to rank what the current viewpoint pays for. Enables agent-driven POV cost surveys via set_camera_pose + breakdown sweeps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Samples the client's real frame times over its own short window (default 2s, no shared profiler state touched) and reports render FPS avg/min/max, hiccup frames (>50ms) and the current scene's tick FPS vs target. Closes the POV optimization loop: set_camera_pose -> get_performance_stats -> get_scene_content_breakdown correlates a viewpoint's content cost with the frame rate it actually produces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An unqualified Time resolves to the DCL.Time namespace in this assembly, breaking compilation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename the scene debug menu metrics panel to "Scene Stats" and restyle it per the design guideline: an in-card title with a "Suggested Specs per Parcel" subtitle and a live "N Parcels = M m²" summary, uppercase row labels, and the five capped metrics first followed by the three uncapped counts. Values now render as "current / cap" with the current value tinted green/yellow and the cap greyed (percentage text dropped); the shared formatter change also applies to the "Scene content" widget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the connection (signal-bars) icon on the Scene Stats opener with a 2x2 grid, drawn as four rounded-square child elements so it needs no new texture asset and stays crisp at any resolution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the drawn 2x2 grid glyph with the existing info.png sprite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a shaderVariants counter (shader + enabled local keywords, the bin URP's SRP Batcher batches draws by) to the content counting pass, scene- wide and per breakdown source. Variant keys are computed allocation-free by caching each shader's keyword space and XOR-combining enabled keyword hashes, so the count works identically in release builds where Unity's render profiler counters are unavailable. Surface it in get_scene_content_stats and get_scene_content_breakdown (new sortBy=shaderVariants), and add interpretation guidance to both tool descriptions and docs/mcp-automation.md: draw-call cost tracks shader variants, not material count — a high material count with few variants is a memory/texture concern, not a frame-time one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Match the panel to the Scene Stats mockup: uppercase header with an inline close button, edge-to-edge alternating row bands, right-aligned monospace values, and capped rows showing current / cap (percent%) — white within budget and orange at or above the warning threshold. Rename the external content row to Videos and drop the parcel-summary subtitle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The panel is opened and closed from its sidebar button, so the header X was redundant. Make DebugPanelView tolerate a panel without a CloseButton. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shared debug-panel background is only 80% opaque, so bright scenes bled through and washed out the values. Give the metrics panel its own near-opaque dark background. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Match the Scene content debug widget label to the Scene Stats panel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
decentraland-bot
left a comment
There was a problem hiding this comment.
PR Review — Scene Content Stats
STEP 2 — Root-cause check
This PR adds a new feature (per-scene content statistics for debugging and automation), not a fix. No root-cause concern applies.
STEP 3 — Design & integration
Problem: Creators and agents need to see how close a scene is to its documented content limits (triangles, entities, bodies, textures) and which assets are the heaviest contributors — without paying for that counting while nobody is looking.
Design verdict: PASS. The architecture is clean and well-placed:
-
SceneContentStatsSystem— a scene-world ECS system that counts content. I searched the repo for existing lifecycle owners:SceneFacadeownsSceneRuntimeMetrics, the GLTF loading pipeline owns containers,PrimitivesRenderingPluginowns primitive meshes. None of these count content stats — this is genuinely new work with no pre-existing owner. The system reads existing components (GltfContainerComponent,PrimitiveMeshRendererComponent,MaterialComponent,PrimitiveColliderComponent,MediaPlayerComponent) for aggregation without duplicating any lifecycle. -
Demand-driven collection — three independent boolean flags (
RequestedByDebugWidget,RequestedByMetricsPanel,RequestedByMcp) gate collection. When all consumers are inactive, the system checks one boolean per frame (CollectionRequested) and does nothing else. When active, it counts every 60 frames. This is the right pattern — explicit creation/destruction moments don't exist for "the current scene's content stats" since the stats are a continuous aggregate, not a lifecycle event. -
Scratch buffers — the
HashSets and dictionaries inSceneContentStatsSystemare cleared on each collection pass and released (cleared) when collection stops. These are per-pass aggregation buffers, consistent with CLAUDE.md §1 ("Can use temporary collections for per-frame aggregation"). TheshaderKeywordsCacheis retained across passes while collection is active (avoids re-allocating keyword arrays) and cleared when collection stops — good trade-off. -
Shared formatter —
SceneContentStatsFormatteris shared between the debug widget and the Scene Stats panel, preventing format drift. Both consumers checkCollectionCountto avoid redundant re-formatting. -
Teardown trace:
DebugViewCurrentSceneSystem.OnCurrentSceneChanged→ clearsRequestedByDebugWidgeton the old scene ✅DebugViewCurrentSceneSystem.Update→ setsRequestedByDebugWidget = contentExpandedeach frame ✅DebugMenuController.OnDisable→ clearsRequestedByMetricsPanelonmetricsScene✅DebugMenuController.UpdateMetricsPanel→ clearsRequestedByMetricsPanelon old scene when scene changes ✅- MCP tools → clear
RequestedByMcpinfinallyblocks ✅ SceneContentStatsSystem.Update→ clears all HashSets/caches whenCollectionRequestedbecomes false ✅
STEP 4 — Member audit
SceneContentStats.CollectionRequested(property) — derived boolean combining three flags. Used bySceneContentStatsSystem.Update. This is a meaningful encapsulation of the demand-flag pattern, not a single-use merge candidate.SceneContentCaps.ForParcelCount(int)(static factory) — used byDebugViewCurrentSceneSystem,DebugMenuController, andGetSceneContentStatsTool. Three consumers — well-justified.SceneContentStatsFormatter.Format/FormatEmpty— shared by the debug widget and the metrics panel. Two consumers — prevents drift.MetricsPanelView.UpdateValues— called byDebugMenuController.UpdateMetricsPanel. Single consumer, but it's a public API on a view class called by its controller — standard MVC separation.
STEP 5 — Line-level findings
One P2 finding (see inline comment). No P0 or P1 issues.
Observations (not blocking):
GetSceneContentBreakdownToolandGetPerformanceStatsToollack unit tests (onlyGetSceneContentStatsToolis tested). The breakdown tool's sorting/limiting and the performance tool's frame sampling would benefit from test coverage.- The
tooltipVisualElement added todocumentRootinMetricsPanelViewis not removed on teardown. This follows the existingDebugPanelViewpattern (no teardown mechanism), and the tooltip is hidden (display: none) when the panel closes, so it's benign.
Security review
No security issues found. The three MCP tools are read-only (McpToolAnnotations.ReadOnly()), tool arguments are clamped (Mathf.Clamp) or validated against enum values, no secrets or credentials are handled, and no file system or network access is introduced.
STEP 6 — Complexity
COMPLEX — introduces a new ECS scene-world system (SceneContentStatsSystem), a new world plugin (SceneContentStatsPlugin), modifies ECSWorldInstanceSharedDependencies and SceneInstanceDeps (dependency injection), adds three MCP tools, two UI surfaces (debug widget + metrics panel), and spans 38 files with ~1,600 additions.
STEP 7 — QA assessment
QA_REQUIRED: YES — runtime debug UI visible in the debug panel and scene debug menu, plus MCP tools that execute in the Unity player.
STEP 8 — Non-blocking warnings
No Main.unity modification detected.
REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: New ECS system, world plugin, shared dependencies change, three MCP tools, two UI surfaces (debug widget + metrics panel)
QA_REQUIRED: YES
Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the text "i" glyph in the metrics panel row info marker with the existing InfoIcn.png sprite, tinted #A09BA8 by default and #FCFCFC on hover. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relocate SceneStats.png into Assets/Textures/UI/Common as icon-scene-stats.png so it joins the auto-generated common sprite atlas, and reference it from the metrics sidebar button via the generated --sprite-common__icon-scene-stats variable instead of a hardcoded asset URL — matching the console/debug icons. Normalize the importer to Single sprite mode (the atlas folder's postprocessor enforces this) and add the generated variable + utility class to Sprites-Common.uss so an in-editor Generate Sprite USS run reproduces an identical file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unity's importer generated the Single-sprite spriteID for the relocated icon-scene-stats atlas sprite. Generate Sprite USS reproduced an identical Sprites-Common.uss, confirming the atlas is in sync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
decentraland-bot
left a comment
There was a problem hiding this comment.
PR Review — feat: scene content stats — debug widget, creator Scene Stats panel & MCP tools
STEP 2 — Root-cause check
This PR adds new debug/profiling functionality — it is not fixing a bug. The change introduces a read-only content-counting system, three MCP tools, a creator-facing Scene Stats panel, and a debug widget. The feature is well-motivated: creators and MCP agents need visibility into scene content budgets. PASS.
STEP 3 — Design & integration
MANDATORY OWNER SEARCH — SceneContentStatsSystem.
The new system counts renderers, meshes, materials, textures, colliders, and media players across the scene's ECS entities. The lifecycle owners I searched:
- GLTF containers —
GltfContainerGroupand its subsystems load/unload GLTF assets. They do not maintain running content counts. - Primitive meshes —
PrimitivesRenderingPlugin/ rendering systems create and destroy primitive renderers. No aggregation. - Materials —
MaterialLoadingGrouploads/unloads materials. No scene-wide counting. - Colliders —
AssetsCollidersPluginmanages collider creation. No aggregation. SceneRuntimeMetrics— already holdsBytesFromScene,BytesToScene,TickTimesNs,TargetFps. AddingContentStatshere follows the established pattern for per-scene profiling data.SceneInstanceDeps— createsSceneRuntimeMetricsatnew SceneRuntimeMetrics()(line 68). It is the lifecycle owner. The PR threadsRuntimeMetricsthroughECSWorldInstanceSharedDependenciesto the scene world — this is the documented pattern for passing per-scene data (CLAUDE.md § "pass per-scene data throughECSWorldInstanceSharedDependencies").
Verdict: No existing owner aggregates content counts across component types. The counting logic spans GLTF renderers, primitive renderers, SDK materials, primitive colliders, and media players — five different component queries that no single existing system covers. A new system is the correct home. The system does not manage any lifecycle; it reads existing components on a throttled schedule. PASS.
Persistent collections: The system holds HashSet<Mesh>, HashSet<Material>, HashSet<Texture>, etc. Per CLAUDE.md §1, "systems must not hold persistent entity/component collections." These are asset deduplication sets (meshes, materials, textures — Unity objects, not ECS entities/components), cleared on each collection pass and fully cleared when no consumer requests collection. They are temporary per-pass scratch buffers reused for allocation-free operation. The shaderKeywordsCache caches shader.keywordSpace.keywords arrays (one allocation per distinct shader, not per material) and is also cleared when collection stops. This follows the "temporary collections for per-frame aggregation" exception. PASS.
Demand-flag pattern: Three booleans (RequestedByDebugWidget, RequestedByMetricsPanel, RequestedByMcp) gate collection. The property CollectionRequested ORs them. Simple and appropriate.
TEARDOWN / CONSUMPTION TRACE:
scenesCache.CurrentScene.OnUpdate += onCurrentSceneChanged→OnDisposeunsubscribes. ✅metricsButton.clicked += OnMetricsButtonClicked→OnDisableunsubscribes. ✅consoleButton.clicked += OnConsoleButtonClicked→ pre-existing: no unsubscribe inOnDisable(not introduced by this PR).info.RegisterCallback<PointerEnterEvent>/<PointerLeaveEvent>inMetricsPanelView→ Cleaned up when the UXML hierarchy is torn down. FollowsConsolePanelViewpattern. ✅RequestedByDebugWidgetcleared inOnCurrentSceneChangedon old scene. Not cleared inOnDispose, but global world disposal coincides with scene world teardown. ✅RequestedByMetricsPanelcleared inOnDisable, on scene change, and when panel is hidden. ✅RequestedByMcpcleared infinallyblocks in both MCP tools. ✅
STEP 4 — Member audit
SceneContentStats.CollectionRequested— 1 consumer (SceneContentStatsSystem.Update). Centralizes the OR of three flags — appropriate.SceneContentCaps.ForParcelCount(int)— 3 consumers. Factory method, correctly shared.SceneContentStatsFormatter.Format()— 3 consumers. Shared formatting, no issues.MetricsPanelView.UpdateValues()— 2 consumers inDebugMenuController. Appropriate.
All members audited — no single-use-merge or absent≠false issues found.
STEP 5 — Line-level findings
See inline comments below.
STEP 6 — Complexity: COMPLEX
39 files changed, ~1800 lines of additions. Touches ECS system registration, plugin wiring, ECSWorldInstanceSharedDependencies, cross-world data sharing, MCP tool layer, and UI toolkit panels.
STEP 7 — QA: YES
Runtime UI changes (debug widget, Scene Stats panel), new ECS system running in scene worlds, new MCP tools.
STEP 8 — Non-blocking warnings
No changes to Main.unity.
CI Status
- ❌ Lint — Warning ratchet failure: 13731 warnings vs baseline 13729. Must remove at least 3 warnings to merge.
- ✅ Tests (editmode + playmode), builds (Windows + macOS), semantic title all pass.
REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Touches ECS system registration, plugin wiring, ECSWorldInstanceSharedDependencies, cross-world data sharing via SceneRuntimeMetrics, MCP tool layer, and UI toolkit panels.
QA_REQUIRED: YES
Reviewed by Jarvis 🤖 · Requested by Juan Ignacio Molteni (<@U03JSUQ5Z7U>) via Slack
- Declare sampleSeconds in get_performance_stats input schema (DescribeInput) - Extract the shared demand-flag wait loop and its timeout constant into SceneContentStatsPolling so the stats and breakdown tools cannot drift - Add GetSceneContentBreakdownTool tests (no-scene error, cleanup finally clears both demand flags, input schema declares limit/sortBy) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pravusjif
left a comment
There was a problem hiding this comment.
Excellent initiative, love the new utility and the new MCP tool, thanks for adding it!
popuz
left a comment
There was a problem hiding this comment.
Nice 👍 Some design questions, can you please address them if possible (non blocking).
DafGreco
left a comment
There was a problem hiding this comment.
✔️ PR reviewed and approved by QA on both platforms following instructions playing both happy and un-happy path
Regressions for this ticket had been performed in order to verify that the normal flow is working as expected:
- [ ✔️] Backpack and wearables in world
- [✔️ ] Emotes in world and in backpack
- [ ✔️] Teleport with map/coordinates/Jump In
- [ ✔️] Chat and multiplayer
- [✔️ ] Profile card
- [ ✔️] Skybox
- [ ✔️] Settings
report created by claude regarding the AI requirement on the PR , so far so good on both platforms and will approve shortly
Evidence
20260807-0852-35.4803456.1.mp4
Samples skipped by the ns <= 0 guard were excluded from the sum but still counted in the divisor, deflating the mean tick duration and inflating the reported averageFps. Count the samples that actually contribute and divide by that, mirroring ComputeTickFps in the debug view. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
get_performance_stats read the whole 256-entry TickTimesNs ring (~8.5 s at 30 tps) regardless of sampleSeconds, so the scene-tick numbers included ticks from before the camera was positioned and polluted the POV correlation the tool promises. Add a monotonic AddedCount to SampledCounter, delta it across the sampling loop, and aggregate only the trailing samples that landed inside the window. Report null when the current scene changed mid-window or did not tick during it (e.g. paused), instead of attributing stale or mixed data to it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tool hardcoded a flat 50 ms bar while the rest of the client defines a hiccup as max(50 ms, 2x target frame time), so on capped configs the MCP hiccup count disagreed with the HUD and telemetry for the same frames. Expose Profiler.EffectiveHiccupThresholdNs and derive the tool's threshold from it, and report hiccupThresholdMs in the output so the count is interpretable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The loop accumulated the requested delay instead of real elapsed time, so at low FPS - the case the generous timeout exists for - each Delay completed a whole frame late and the effective timeout drifted well past the intended 10 s, into the dispatcher's 30 s kill. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cel each other McpHttpServer dispatches tool calls concurrently, so two content-stats calls can overlap; with a single bool the first finally cleared the shared demand, the counting pass stopped, and the other call stalled to its timeout on a healthy scene. BreakdownRequested had the same flaw, plus the system's one-shot clear stomped a second waiter's demand. Replace both bools with main-thread refcounts (McpRequests, BreakdownRequests): each tool call increments before waiting and releases only its own count in finally, and the system reads count > 0 instead of clearing. Interlocked is unnecessary - the tools declare RequiresMainThread, so overlap is async interleaving on one thread. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The behavioural tests all passed collectionTimeoutMs: 0, so the wait body - pass landing, scene-changed detection - never executed, the happy paths were unreachable, and get_performance_stats had no tests at all. Give the content tools an injectable WaitForCollection delegate (defaulting to SceneContentStatsPolling.WaitForCollectionAsync) and extract get_performance_stats' frame loop behind a SampleFrames delegate, then cover: render-stats math from a sampled window, the tick-window slicing (stale ring ticks excluded, null on no ticks or scene change mid-sample), sampleSeconds clamping, the shared hiccup threshold, breakdown sorting and share math, sortBy=visibleTriangles, limit clamping, and scene-changed errors on both content tools. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- SceneContentStatsSystem: GetInstanceID() is obsolete in Unity 6000.4; use GetEntityId() for the shader variant key (CS0618). - DebugMenuController: visiblePanel and inputBlock are genuinely null at times (no panel open; before Initialize runs), so annotate them nullable and guard the OnEnable forward of inputBlock instead of passing null into the console view (CS8618/CS8625 + always-true). - DebugMenuSettings.UiDocumentPrefab: inspector-assigned required reference, initialized null! per the serialized-settings convention (CS8618). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tool returned structuredContent without declaring outputSchema in tools/list, breaking the server invariant that every structured tool declares one - clients that validate against the schema would reject or drop the payload. Add McpJsonSchema.ObjectArray for the entries array, declare the full schema, teach McpSchemaAssert to recurse into array items so the ranking test guards schema-payload drift, and fix the doc paragraph that listed neither this tool nor get_performance_stats among the structured ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🚦 CI StatusWindows and Mac build successful in Unity Cloud! You can find a link to the downloadable artifact below. Warnings count reduced: 13722 => 13715 Warnings/errors in files changed by this PR (87)All Unity tests passed ✅
|
Pull Request Description
What does this PR change?
Adds per-scene content statistics to a new "Scene content" debug widget so creators and devs can see, at a glance, how close the scene they are standing in is to its limits. It lives next to the existing "Current scene" widget (which keeps the runtime tick/FPS/traffic metrics) as a separate widget, so watching performance does not pay for content counting and vice versa — the counting pass only runs while a consumer is showing the stats. Rows with a documented scene limitation are formatted
current / cap (percent%), the whole value colored white (< 80%) or orange (≥ 80%) — the docs define these as soft limits ("reported as warnings by the Creator Hub... treat them as strong recommendations"), so exceeding one warns but never shows red. Rows without a documented limit are shown as plain counts:EntitiesMap.Count) (capped)Meshinstances; low relative to bodies signals efficient instancingVideoPlayer/AudioStreamcomponentIntentionally omitted — Content size. An earlier revision showed a "Content size" row that summed runtime memory of unique meshes + textures (
Profiler.GetRuntimeMemorySizeLong) against the 15MB/parcel budget. It was removed: the deployed-size budget is validated at deploy time (catalyst) and by the SDK tooling, so it is not this client widget's responsibility — and runtime memory is a different quantity anyway (decompressed textures dwarf their deployed size, so the row showed red on deploy-compliant scenes).Caps are hardcoded from the documented Decentraland scene limitations, scaled by parcel count
n— four of the five formulas on that page: trianglesn×10000, entitiesn×200, bodiesn×300, textureslog2(n+1)×10. Materials is deliberately shown uncapped even though the docs give it a formula (log2(n+1)×20) — see Why shader variants? below. Geometries, colliders and external videos/audios have no cap and are shown as plain counts: geometries only had a formula in the legacy SDK6-era docs (dropped from the current page), and colliders/media were never documented. All caps live in oneSceneContentCapsstruct inSceneContentStatsFormatter.cs.Agent/automation surface — three MCP tools. The embedded MCP server gains read-only tools documented in
docs/mcp-automation.md:get_scene_content_stats— the same stats as structured JSON (values + the documented caps for the scene's parcel count + afreshflag), plus ashaderVariantscount (shader + enabled keywords, deduped scene-wide). It sets its own collection-demand flag and waits for the scene world to complete a counting pass (CollectionCountstamp), so it works with every stats UI closed — agents can assert budgets programmatically instead of reading screenshots.materialsis reported without a cap for the same reason the UI drops it. Covered byGetSceneContentStatsToolShould.get_scene_content_breakdown— content grouped by source model (GLTFsrc+ one primitives row), ranked bytriangles/materials/shaderVariants/drawCalls/visibleTriangles: per source the summed triangles, instances, renderers, unique materials, shader variants, a pre-batching draw-call estimate, and the visible-from-this-POV subset (post-culling perRenderer.isVisible) — answers "what should I optimize" and "what does this viewpoint pay for". Filled by the same counting pass via a one-shot flag; costs nothing unless requested.get_performance_stats— samples real frame times over its own short window (default 2 s) and reports render FPS avg/min/max, hiccup frames (>50 ms) and the scene's tick FPS vs target. Together the three close the loop: position the camera, measure FPS, rank the visible content that produces it.Why shader variants? (and why materials is uncapped). URP's SRP Batcher bins draws by shader variant, not by material — many materials sharing few variants render cheaply, so a high material count alone signals memory/texture cost and lost instancing opportunities, not draw-call cost. A per-parcel cap on materials would therefore misinform more than it helps, so materials is shown as an informative count on every surface (UI rows and the MCP
materialskey) with no cap. Agents reading raw material counts were drawing the wrong conclusion ("every unique material is an unbatchable draw call"); the variant count is the release-build-safe number that settles it (Unity's ProfilerRecorder render counters — real SetPass/batches — are stripped from release builds, which is what creators run). Both tool descriptions carry this interpretation guidance, anddocs/mcp-automation.md§ "Interpreting the numbers" documents it for humans.Creator-facing surface — Scene Stats panel. The same stats are also shown to creators through a third sidebar button (stats-chart icon) in the scene debug menu at the top right, next to the console and debug-panel buttons. That menu only exists in local scene development mode or with
--scene-console, so this reaches creators previewing their scene without exposing anything to regular players. The panel (MetricsPanelView) is a dark "Scene Stats" card with edge-to-edge banded rows: uppercase labels on the left, right-aligned monospace values on the right, capped rows showingcurrent / cap (percent%)in white or orange. Each row carries a small info marker whose hover tooltip explains what the metric indicates — capped rows note the per-parcel limit and the white/orange budget colouring, materials explains why it is uncapped (SRP Batcher), the rest describe what they count. The tooltip is parented to the document root so the panel'soverflow: hidden(rounded corners) doesn't clip it; it floats in the free space to the panel's left, positioned once on hover and hidden on leave / when the panel closes. Row formatting and caps live in a sharedSceneContentStatsFormatter(DCL.Profiling) so both UIs cannot drift apart.Architecture: a new scene-world system (
SceneContentStatsSystem,SyncedPresentationSystemGroup) counts every 60 frames and writes into a newSceneContentStatspayload onSceneRuntimeMetrics(now exposed to scene-world systems viaECSWorldInstanceSharedDependencies). Collection runs on demand: the "Scene content" widget (DebugViewCurrentSceneSystem), the metrics panel (DebugMenuController) and the MCP tools each set their own request flag only while active — with all closed, the scene system does a single bool check per frame and nothing else. Consumers re-format only when a new pass lands (they trackCollectionCount), so display work is zero between passes. Counting is allocation-free (reusedHashSets / scratch list, non-allocGetSharedMaterials); only explicitly requested breakdown passes allocate.Test Instructions
Steps (standard run):
Expected result:
The new "Scene content" debug widget shows the eight stat rows with colored
current / cap (percent%)values for the scene you are standing in; the "Current scene" widget keeps the runtime tick/FPS/traffic metrics.Steps (fresh account):
Expected result:
Same as above after finishing onboarding.
Automation (if applicable):
N/A
Prerequisites
--mcp, e.g.:--realm http://127.0.0.1:8000 --position 0,0 --local-scene true --skip-version-check true --mcpTest Steps
Scene content widget
—for up to ~1 second, then populate against Genesis Plaza's parcel count — capped rows (triangles, entities, bodies, textures) ascurrent / cap (percent%)colored white or orange, uncapped rows (materials, geometries, colliders, external videos/audios) as plain counts.Scene Stats panel
6. Click the stats-chart button in the top-right sidebar — a dark "Scene Stats" card opens with banded rows and the same eight Genesis Plaza values; hover any row's info marker and a tooltip explains that metric; close the card and collection stops (unless the debug widget is also open).
7. With both open at once, confirm the Scene content debug widget reflects the same eight values as the Scene Stats panel — every row (capped and uncapped) matches between the two for the same Genesis Plaza scene.
MCP tools (launch with
--mcp)8. With all stats UIs closed, point a coding agent at the running Explorer (loaded into Genesis Plaza) and paste this prompt:
Additional Testing Notes
shaderVariants, not the material count (see "Why shader variants?")Finishedstate, so values grow while a scene streams inQuality Checklist
Code Review Reference
Please review our Branch & PR Standards before submitting. It explains the automated review flow, QA/DEV approval requirements, and what each label does — especially useful for first-time contributors.
🤖 Generated with Claude Code