Skip to content

Implement instanced prism rendering via Entities Graphics#573

Open
YsKhan61 wants to merge 24 commits into
claude/confident-clarke-f99p6rfrom
claude/zen-volta-t9su0i
Open

Implement instanced prism rendering via Entities Graphics#573
YsKhan61 wants to merge 24 commits into
claude/confident-clarke-f99p6rfrom
claude/zen-volta-t9su0i

Conversation

@YsKhan61

@YsKhan61 YsKhan61 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the rendering half of the prism ECS migration (Phase R from Docs/PRISM_ECS_MIGRATION.md), replacing per-prism MeshRenderer + MaterialPropertyBlock draw calls with Entities Graphics instanced batching. This directly addresses the end-of-round frame collapse caused by N live prisms ≈ N draw calls + N SetPass operations.

Key Changes

Core Rendering Bridge

  • PrismRenderService: Static service managing companion ECS entities for prism rendering. Provides Create(), SetVisible(), SetColors(), SetTransform() APIs that the legacy GameObject path calls through. Includes master toggle (opt-in via PrismRenderConfigSO or runtime override) and diagnostic status reporting.
  • PrismRenderHandle: Opaque handle (Entity + epoch) stored on Prism MonoBehaviour, validated before dereferencing to catch stale references across world resets.
  • PrismRenderProperties: Per-instance shader property overrides (_BrightColor, _DarkColor, _Spread) that Entities Graphics uploads into persistent DOTS-instancing buffers.

Prism Integration

  • Prism.cs: Added RenderHandle, _renderVisible, _exoticVisualActive state. Single SetRenderVisible() entry point routes both render paths (entity vs. GameObject). UsesEntityColorSink property gates whether MaterialStateManager writes to the entity or a MaterialPropertyBlock. Octahedron shield morphing (_exoticVisualActive = true) hands rendering back to the GameObject for per-frame mesh swaps.
  • PrismOctahedronShield.cs: Calls Prism.SetExoticVisualActive() on engage/disengage to toggle between entity and GameObject rendering.
  • MaterialPropertyAnimator.cs: Exposed CurrentBrightColor, CurrentDarkColor, CurrentSpread for entity path to read back the last animated state (interruption continuity).
  • MaterialStateManager.cs: Routes color updates to either MaterialPropertyBlock (legacy) or PrismRenderService.SetColors() (entity path) based on Prism.UsesEntityColorSink.

VFX Integration

  • PrismExplosion.cs & PrismImplosion.cs: Mirror the prism path — companion entities with animated shader property overrides (_Velocity, _ExplosionAmount, _Opacity for explosions; _State, _Location for implosions) so 64 simultaneous effects batch into one instanced draw instead of 64 separate ones.
  • PrismEffectsManager.cs: Added MAX_ACTIVE_EFFECTS = 256 hard ceiling (profiled at ~97ms/frame when unbounded); recycles oldest effect under extreme load.

Stress Testing & Diagnostics

  • PrismRenderStressTest.cs: Max-prism harness spawning N render entities (no GameObjects) with per-instance color overrides. Two modes: via PrismRenderService (measures bridge overhead) or raw EntityManager (theoretical ceiling). Publishes live stats to DiagnosticsHUD.
  • PrismStressInjector.cs: Dev-only (F10 toggle) injects a stress cloud on top of any scene for real-world load testing without scene authoring.
  • DiagnosticsHUD.cs: Added external stats API (SetStat(), SetSection()) so stress harnesses and perf probes publish rows instead of drawing overlays.

Configuration & Fallback

  • PrismRenderConfigSO.asset: Opt-in toggle (defaults OFF; legacy path is baseline until ShaderGraphs expose animated properties as Hybrid Per Instance).
  • PrismRenderService.Enabled: Resolution order: runtime override → config asset → default OFF. Logs activation and diagnostic hints.
  • All methods are main-thread only and no-op safely when ECS world or EntitiesGraphicsSystem is unavailable (

https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A

claude and others added 14 commits June 29, 2026 23:59
The instanced path (PrismRenderService) no-ops silently when the config asset,
ECS world, or EntitiesGraphicsSystem is missing, so a 'still N draw calls'
symptom gave no on-screen signal of WHY entities weren't drawing. Add
PrismRenderService.StatusLine() — a read-only diagnosis of the exact broken
link (toggle off / no asset / no world / no graphics system / ON+ents=N) — and
render it as a 'Prism Path' row in the DiagnosticsHUD Render section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
PrismRenderService is the project's only runtime Entities usage in a gameplay
scene, so if Unity's automatic world bootstrap doesn't produce a default world,
TryEnsure() failed and every prism silently fell back to its MeshRenderer
(symptom: 'ACTIVE' logged but draw calls still scale 1:1 with prism count).
TryEnsure() now creates the default world on demand via
DefaultWorldInitialization.Initialize when none exists — guarded on null so it
can never double-create, and Initialize hooks the world's system groups
(EntitiesGraphicsSystem included) into the player loop. No subscene or scene
authoring is required for the instanced path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
FastGrowPrism prisms render identically with the instanced path ON or OFF
(non-instanced 'Draw Mesh', MPB-driven) even though ents>0, i.e. they never
get a companion render entity. Add a rate-limited (12) warning in
Prism.EnsureRenderEntity reporting the exact bail reason (null renderer/mesh/
material, or Create returning invalid) so the gap can be pinned in one replay.
Temporary — remove once resolved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
…adout

ents>0 with draw calls ~= ents and Batches == Draw Calls means the entities are
created but Entities Graphics draws each individually (no instanced batching).
The batching ceiling is the distinct (mesh x material) count, which the service
already caches. Append meshes=/mats= to StatusLine so we can tell in one glance
whether prisms fail to share a mesh (meshes huge) or the shader variant isn't
instancing (meshes/mats tiny but draws still ~= ents).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
Every spindle (tadpole bodies, gyroid branches) set a per-renderer
MaterialPropertyBlock for a random _Phase, which excludes it from the SRP
Batcher — so hundreds of spindles each drew as their own draw call (the
~600 un-batched BezierCurve draws in the transparent pass). Replace the MPB
with a small pool of SHARED phase-variant materials (8 per base material)
selected by a world-position hash: sway stays desynced, but every spindle in
a phase bucket shares one material and batches into a single draw. Purely
cosmetic; no ecology/gameplay change. Base material captured once so pooled
reuse can't layer variants-on-variants; RestoreOriginalMaterial now assigns
sharedMaterial so stable-life spindles stay batchable after birth/death.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
So it runs standalone in an empty scene (mirrors the on-demand world creation
in PrismRenderService). This is the isolation harness for the 'entities don't
instance-batch' investigation — pure render entities, one mesh + one material,
no game code — to split a config/shader batching bug from a game-integration one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
Authored scene: camera (far-clip 5000, back at z=-1500 to view the spawn
cloud) + light + a PrismRenderStressTest wired to the shared prism mesh
(Prism.asset), a BlockGraph material (BlueBlockMateral), count=50000. Open and
press Play to read Draw Calls / Batches / SetPass on the DiagnosticsHUD — this
isolates whether Entities Graphics instancing works in this project's config,
independent of any game code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
… Shore

FrogletTools/Legacy/TestingMultiplayer/{Load,Do not load} Bootstrap Scene on
Play -> Tools/Cosmic Shore/Play Mode/. Same EditorPrefs key, so the current
setting and the benchmark tool's programmatic access are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
… F10 injector

- DiagnosticsHUD gains a static SetStat/ClearStats API: any system publishes
  rows into titled sections of the HUD instead of drawing its own OnGUI
  overlay (no-op in release; the HUD is editor/dev-only).
- PrismRenderStressTest: OnGUI overlay removed — numbers go to the HUD. New
  default spawn mode routes every entity through PrismRenderService (Create/
  SetVisible/SetColors/SetTransform), the same bridge game prisms use, so the
  Prism Path ents/meshes/mats line reflects the population and the service's
  own overhead is measured; useRenderService=false keeps the raw EntityManager
  path as the zero-overhead ceiling A/B. Configure() lets runtime code set it
  up before Start.
- PrismStressInjector (editor/dev only, auto-spawns): press F10 in ANY scene
  (Menu_Main lava-lamp, benchmark scene, a race) to toggle a 50k instanced
  stress cloud in front of the camera, borrowing mesh + themed material from
  a live prism. Zero scene authoring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
…red meshes

Root cause of the in-game draw-call storm (menu lava-lamp): every shielded
prism spent its WHOLE shielded lifetime on the GameObject renderer with a
per-prism generated octahedron mesh (SetExoticVisualActive(true) for the
duration), so the Squirrel crystal-collect rings (8 permanently-shielded
prisms per pickup) and crystal-adjacent trail prisms accumulated hundreds of
un-instanced draws with the exact Frame Debugger break 'Rendering different
meshes or submeshes with GPU instancing'.

Fix, render-side only (no gameplay/ecology change, universal — not a menu
carve-out):
- OctahedronMeshGenerator.GetSharedShieldMesh: settled-shield octahedra come
  from a cache keyed by quantized (halfExtents, shieldScale). Half-extents are
  the authored LOCAL collider size, so same-prefab shields share ONE mesh
  (convex MeshCollider also cooks once). Stale-cache guard for domain-reload-
  off play sessions.
- PrismRenderService.SetMesh: swaps a companion entity's mesh + RenderBounds.
- Prism render-mesh override: while set, the entity renders the shared
  octahedron instead of meshFilter.sharedMesh; EnsureRenderEntity honors it,
  Initialize clears it on pooled reuse.
- PrismOctahedronShield: the settled pose hands rendering BACK to the entity
  (SetRenderMeshOverride + SetExoticVisualActive(false)); only the engage
  morph and shatter overlay — genuinely per-prism geometry — keep the
  GameObject renderer. Engage/disengage/bloom/shatter visuals unchanged
  (same geometry, same materials; all shielded materials are BlockGraph,
  already Hybrid-Per-Instance). Legacy path behavior identical.

Also in this commit:
- fix(sparrow): Sparrow Projectile Prism.prefab had an EMPTY DefaultLayerName
  -> LayerMask.NameToLayer('') = -1 -> ArgumentException in Prism.Awake that
  killed the Sparrow's full-auto fire loop. Prefab now uses TrailBlocks and
  Prism guards invalid layer names with a warning instead of throwing.
- fix(spindle): phase-variant material cache rebuilds when a stale entry
  survives play-mode exit with domain reload disabled (destroyed fake-null
  materials -> magenta spindles + stalled wither cascade).
- chore(prisms): remove the TEMP LogEntityDiag diagnostic (proven unable to
  fire for the real issue).
- benchmark scene: all four Cell config slots now use the menu's Blob Cell
  Config so the flyable benchmark environment matches the lava-lamp ecology.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
…rk scene creator

Corrections per review:
- The bootstrap play-mode toggles keep their original submenu name under the
  new home: Tools/Cosmic Shore/Testing Multiplayer/ (was briefly 'Play Mode').
- Delete CreateBenchmarkSceneTool (+meta): BenchmarkStressTest.unity is
  committed to the repo — there is exactly one benchmark scene and nothing
  should re-create or overwrite it. Stale doc/comment references updated
  (BenchmarkSceneLauncher summary, Docs/SettingsSystem/ARCHITECTURE.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
CS1061: the Dictionary key type declared named elements (x,y,z,s) but the
local was built as an unnamed tuple, so key.x didn't exist. Construct the
tuple with names. Audited the rest of the pass for the same class of error:
the only other tuple (PrismStressInjector.FindDonor) declares names in its
return type and is deconstructed at the call site — correct as written.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7d51d786c3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +203 to +207
if (meshRenderer && meshRenderer.enabled) meshRenderer.enabled = false;
if (show)
{
SyncRenderMaterial();
SyncRenderTransform();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep entity transforms in sync for direct movers

With instancing enabled by the new Resources config, this branch disables the MeshRenderer and the entity only receives LocalToWorld when ApplyRenderPath/SyncRenderTransform runs. I checked ShapeDrawingManager.ShrinkPrismsIntoShape, which directly assigns prisms[i].transform.position and .localScale each frame (Assets/_Scripts/Controller/Environment/MiniGameObjects/ShapeDrawingManager.cs:449-450) without NotifyPositionChanged or SyncRenderTransform; those captured prisms will keep rendering at their old entity transform during/after the shrink animation. Route those direct movers through the sync API or add a transform-dirty sync.

Useful? React with 👍 / 👎.

// but refreshed only when the prism's scale changes, so this whole-
// population recompute no longer does a transform.lossyScale parent-walk
// per prism (the ~23ms/recompute hotspot at high prism counts).
float v = prism.CachedVolume;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refresh cached volume for direct scale changes

Switching the cell volume recompute to CachedVolume makes volume consumers stale for scale changes that do not go through PrismScaleManager. ShapeDrawingManager.ShrinkPrismsIntoShape directly writes transform.localScale every frame (Assets/_Scripts/Controller/Environment/MiniGameObjects/ShapeDrawingManager.cs:450) and never calls RefreshVolumeCache, so Cell.LiveVolume, phase, and dominant-domain calculations keep using the pre-shrink volume for those prisms. Either keep a live-volume fallback for uncached paths or refresh the cache wherever prism scale is assigned directly.

Useful? React with 👍 / 👎.

claude added 10 commits July 2, 2026 02:31
F10 collides with the recorder shortcut, so the stress injector is now driven
by a console instead of a key: the HUD gains an input field + Run button
(Enter also submits; onEndEdit gated on an actual Enter press so focus loss
doesn't fire commands). Commands are pluggable via a static
RegisterCommand/UnregisterCommand registry — handlers get the parsed args and
return a one-line result shown in a 'Console' section and logged. HUD hotkeys
(F5/F6/F7) are suppressed while the field has focus.

PrismStressInjector registers 'prisms': 'prisms 50000' spawns an instanced
stress cloud of that size ahead of the camera (replacing any existing cloud),
'prisms' uses the default count, 'prisms off' removes it. Same behavior as
the old F10 toggle, now parameterized per-invocation.

No ecology assets changed: the menu (Blob) fauna spawn probabilities were
verified already 1 for all three fauna configs, the CellConfig->SpawnProfile
reference chain is intact, and no commit on this branch touches fauna,
spawner, or Menu_Main files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
The stress harness painted its cloud with hardcoded approximations
(jade-ish/ruby-ish/gold-ish literals, dark forced to black) — the only prisms
not sourced from the project's theme. The palette is now resolved from
ThemeManagerDataContainerSO.ColorSet (Jade/Ruby/Gold InsideBlockColor ->
_BrightColor, OutsideBlockColor -> _DarkColor — the exact mapping ThemeManager
bakes into every domain block material), so injected clouds in the menu/
benchmark match real prisms per-channel. Tool scenes with no theme loaded
fall back to the assigned material's authored colors (also project-authored).
Color churn now pulses the theme bright color and keeps the theme dark color
instead of black.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
DOTS-instanced per-instance data uploads verbatim, but the legacy path's
colors went through Unity's color-space handling (every legacy prism carried
a per-renderer MaterialPropertyBlock — the converted look IS the reference
look). Writing the same authored numbers raw read brighter, most visibly on
the dark 'outside' color, which no longer sat darker than the inside.

Fix at the single entity-write boundary in PrismRenderService: ReadColor
(create + material refresh), SetColors (animated), and SetTeamColors
(effects) now run authored colors through GammaToLinearSpace when the
project is in Linear color space; _Spread and the non-color effect params
are untouched, and the legacy MPB/material path is untouched. The stress
harness's direct-EntityManager mode applies the same transform via the
public ApplyColorSpace helper.

In-editor A/B without recompiling: console command 'prismcolors raw'
reproduces the too-bright pre-fix look for newly written colors,
'prismcolors linear' forces conversion, 'prismcolors auto' returns to the
project default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
…e hot paths

Frame-spike diagnosis follow-ups:

- PrismEffectsManager zombie audit: the periodic FindObjectsByType full-scene
  scans (two per audit; ms-scale with thousands of scene objects — the
  reported Update() spike) are replaced by O(live effects) enabled-instance
  registries on PrismExplosion/PrismImplosion (OnEnable add / OnDisable
  remove). Iterated backwards because force-deactivating a zombie removes it
  from the registry mid-walk. Audit semantics unchanged; editor/dev only.

- ImpactorBase: per-concrete-type ProfilerMarker around AcceptImpactee
  (lazy — no Awake added; subclasses own theirs), so the reported ~24ms
  OnTrigger frames attribute to 'SkimmerImpactor.AcceptImpactee' etc. in the
  next capture instead of vanishing into Physics.SendEvents self-time.

- CapsuleMembrane: split markers for UpdateMatrices (CPU math, scales with
  capsule count — subdivisions 4 ~= 2,562 capsules) vs RenderMeshInstanced
  (matrix-array copy to the render thread) to separate math cost from
  render-thread sync in the reported intermittent spike.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
…rame

SnowChanger.ChangeSnowOrientation ran an O(shardCount) transform.LookAt loop
(~4,189 shards in the menu cell at shardDistance=120, membrane radius 1200)
INLINE in the OnCellItemsUpdated SOAP raise — which the crystal-pickup chain
fires from a physics callback. Profiled as the dominant chunk of the 14.92ms
self-time on the pickup frame. The raise now only rewinds a cursor; Update
reorients a bounded slice (shardsPerFrame, default 128) until done — same
visual result spread over a few frames, zero allocation, and the pickup frame
no longer pays for the cytoplasm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
The Squirrel crystal-pickup ring (SpawnableRings, 3 rings x 8 prisms)
Instantiated 24 FastGrowPrism GameObjects inline in the impact frame and
parented them under a container GO that was never destroyed - one leaked
AOERingSpawner root per pickup for the session.

- PrismType gains explicit values (serialization drift guard) + FastGrow=10;
  PrismFactory routes it to a new FastGrowPrismPool (InteractivePrismPoolManager
  on PrismManagers.prefab, capacity 24/max 100, same cleanup events as the
  other prism pools).
- SpawnableRings spawns via the established PrismEventChannelWithReturnSO
  instead of Instantiate: pooled checkout, world-space pool parenting, no
  container GO. Ring prisms get a monotonic spawn-serial ownerID so IDs
  never collide across detonations.
- Shield/danger flags are written (both ways) onto prismProperties before
  Initialize so pooled reuse can't leak a prior life's state and the shield
  engages exactly once inside Initialize.
- AOEBlockSpawner destroys itself after its spawn loop, matching the
  AOEExplosion base contract - no more per-pickup spawner roots.
- Both explosion-cooldown SOs key their static dictionaries by
  GetInstanceID() so destroyed impactors are never retained across scenes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
…ones

Every crystal pickup Instantiated one husk per model (4 for the omni
crystal), cloned a Material per husk, mutated the clone per frame from
Impact's drift coroutine, then Destroyed both - allocation churn and
object churn in the same frame as the pickup impact.

- Impact drives its shader state (_player/_red/_velocity/_Opacity)
  through a MaterialPropertyBlock over the SHARED exploding material -
  no clone, no material Destroy. Unset properties fall back to authored
  values, matching the legacy per-clone semantics exactly. Pooled
  instances raise OnReturnToPool when the drift completes; unpooled
  fallbacks keep the legacy self-destroy.
- New SpentCrystalPoolManager (GenericPoolManager<Impact>): one instance
  per spent prefab on PrismManagers (omni dummy + 4 elemental dandruffs),
  self-registered into a prefab-keyed registry so Crystal.Explode and
  Mine.Explode resolve their serialized spent prefab with zero wiring.
  Unregistered prefabs fall back to Instantiate. Scene changes release
  checked-out husks (deactivation kills the drift coroutine before it
  can hand itself back).
- GenericPoolManager exposes a read-only Prefab accessor for the registry.
- FadeIn (crystal-model respawn fade) animates _opacity via MPB with a
  cached Renderer - the old path cloned the material in Start and did a
  GetComponent<Renderer> per frame. The override clears at fade end so
  material swaps (activation, domain change) show authored opacity.
- CrystalManager caches the intensity-selected anchor array instead of
  ToArray() twice per respawn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
…on reporter

The Squirrel hull carries two BoxColliders, so one crystal pass raised
OnTriggerEnter once per collider pair and ran the entire vessel-side
crystal effect chain twice - double AOE ring detonation, double SFX,
double stat writes. VesselImpactor now latches per crystal instance for
0.5s (the same window Crystal holds IsExploding), before the network
RPC dispatch so host and client paths dedupe identically.

VesselCollisionReporterSO was wired into the Squirrel's crystal effect
list but raised EventOnSkimmerVesselCollision - a skimmer-vessel event -
on every crystal pickup, polluting whatever legitimately listens to that
channel (StatsManager) plus a per-pickup debug log. The event channel and
its real raisers stay; the reporter class, its asset, and the container
entry are deleted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
Ring prisms are conserved mass - they never return to the pool mid-game,
so every pickup permanently checks out 24 and the buffer refills at
~20/s. A second pickup within ~1.2s hit the shortfall as synchronous
in-frame Instantiates. Buffer target 24 -> 72 covers three chained
pickups; the extra 48 instances prewarm behind the Bootstrap splash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
First-use effects (crystal pickups, explosions, trails) hitch on in-game
shader variant compilation - the one first-pickup cost pooling can't
touch. AppManager now warms every ShaderVariantCollection wired on
BootstrapConfigSO right after the bootstrap settle frames, while the
splash overlay is still opaque and before the minimum-splash wait, so
the compile cost is absorbed into the hold the player already sits
through. Variants stay compiled for the session.

Ships with an empty GameplayShaderWarmup collection wired (WarmUp on an
empty collection is a no-op). Populate it from a representative editor
play session via Project Settings > Graphics > Shader Loading >
"Save to asset..." over the same path - the guid is preserved so the
BootstrapConfig wiring keeps pointing at it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zxC135NV3WsroPRsfej1A
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.

2 participants